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.
blog/security-responseA 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.
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-runUseful 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
| Concern | runbook + tabs | OrchStep |
|---|---|---|
| Triage decision | per-person judgment | one if: on severity |
| Scanner flakes | retry by hand, or skip | retry: with backoff |
| Ticket -> notify | two manual steps | notify reads the ticket outputs: |
| Every scan recorded | only when remembered | finally: 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
- Error Handling —
retry:,catch:,finally:, timeouts - Conditionals & Branching — triage with
if:/then:/else: - Variables & Outputs — linking steps via
outputs:
Got a vulnerability runbook that depends on who's on shift? Make the response one workflow and run it the same way every time.
curl -fsSL https://orchstep.dev/install.sh | sh