BLOG/ENTERPRISE AUTOMATION
THEME
ENTERPRISE AUTOMATION

Security response automation

The vulnerability response that lives in five browser tabs and a runbook nobody follows the same way twice. Here's scan -> triage -> ticket -> notify as one ordered OrchStep workflow.

Apr 26, 2026 OrchStep Team 6 minROLE: Security EngineerSCALE: Enterprise
RUNNABLE DEMO
Full source for this post: blog/security-response
VIEW SOURCE

A scanner flags a CVE in a production image. What happens next is the part that's never written down the same way twice: someone reads the severity, decides whether it crosses the bar, maybe opens a ticket, maybe pings a channel, maybe forgets the channel. Five tabs, one runbook, and a response time that depends entirely on who's on shift.

The scan is automated. The response is not. This post turns the response into one ordered OrchStep workflow — scan, triage by severity, ticket when it crosses the threshold, notify — so the path from "scanner fired" to "ticket filed and team told" is the same every time.

The path

The interesting step is triage: an if: on the scan's severity. Above the bar, it opens a ticket and notifies. Below it, it logs and moves on. A finally: records every scan in the audit trail either way.

orchstep.yml
name: secresponse
# Scan an image, triage by severity, open a ticket when it crosses the bar,
# and notify the channel — one ordered path instead of five manual tabs.
defaults:
  image: ghcr.io/acme/api:1.4.0
  threshold: high

tasks:
  # `orchstep run respond --var image=ghcr.io/acme/api:2.0.0`
  respond:
    steps:
      - name: scan
        func: shell
        do: echo "scanning {{ vars.image }} for known CVEs"
        retry:
          max_attempts: 3
          interval: "1s"
          backoff_rate: 2.0
        outputs:
          severity: high

      - name: triage
        if: '{{ eq steps.scan.severity vars.threshold }}'
        then:
          - name: ticket
            func: shell
            do: echo "opening a {{ steps.scan.severity }}-severity ticket for {{ vars.image }}"
            outputs:
              id: "SEC-4711"
          - name: notify
            func: shell
            do: echo "posting {{ steps.ticket.id }} ({{ vars.image }}) to the security channel"
        else:
          - name: log
            func: shell
            do: echo "{{ vars.image }} below the {{ vars.threshold }} bar — logging only"
        finally:
          - name: audit
            func: shell
            do: echo "scan of {{ vars.image }} recorded in the audit trail"

The demo emits a literal severity so it runs anywhere. In production, scan shells out to your scanner (Trivy, Grype, the registry's own API) and the outputs: block captures the highest severity it found.

Two things this fixes

The scanner flakes, and that shouldn't drop the response. Registry scans time out. The retry: block gives scan three attempts with exponential backoff — max_attempts: 3, interval: "1s", backoff_rate: 2.0 — so a transient failure doesn't silently skip triage. That's the whole retry, as syntax, not a hand-rolled loop.

The ticket and the notification are linked. The notify step references the ticket step's id output — it can't post to the channel without the ticket ID, because the ticket step produced it. You don't get a Slack ping that says "something's wrong, go find the ticket." You get the ticket number, every time, by construction.

The whole thing is driven off two variables: the image and the threshold. Raise the bar to critical for a noisier service with --var threshold=critical; the workflow is identical.

Preview the branch before it runs

A dry run resolves the severity and threshold and shows whether a given image takes the ticket path or the log-only path — before it opens anything:

orchstep run respond --var image=ghcr.io/acme/api:2.0.0 --dry-run

Useful when you're tuning the threshold and want to see what would file a ticket without filing one. More: Previewing with Dry Run.

What you gained

Concernrunbook + tabsOrchStep
Triage decisionper-person judgmentone if: on severity
Scanner flakesretry by hand, or skipretry: with backoff
Ticket -> notifytwo manual stepsnotify reads the ticket outputs:
Every scan recordedonly when rememberedfinally: audit step

This isn't a SOAR platform, and it doesn't try to be. It's the ordered, repeatable response path you already wrote in a wiki — made executable, so on-call runs it the same way at 3am as you do at noon.

Where to go next

Got a vulnerability runbook that depends on who's on shift? Make the response one workflow and run it the same way every time.

#SECURITY#VULNERABILITY#INCIDENT-RESPONSE#AUTOMATION
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — ENTERPRISE AUTOMATION