
SOAR is a playbook that runs after a detection, not a detection itself.
If the SIEM never saw the event, the SOAR cannot enrich it. If the playbook has a write action (disable a user, block an IP) without a human gate, a bad detection becomes an outage.
The usual mistake is buying orchestration and never writing the one playbook that closes a KEV item on the actual host.
This page is where SOAR helps, where it is theater, and the playbooks that should exist before you add more integrations.
CISA’s Known Exploited Vulnerabilities catalog showed 1,674 rows when it on 22 August 2026. CVE-2026-73570 in Zimbra was added on 21 August with a due date of 24 August. Gartner’s 2019 SOAR Market Guide said fewer than 5 percent of organizations used the tools and predicted more than 30 percent by the end of 2022. I could not find a first-party 2026 Gartner figure that replaces that prediction, so the 30 percent line stays in 2019.
The copy that used to live here opened with basement hackers, a 2019 FBI complaint count, COVID, and a JetPatch pitch. Those are not controls. Pair the close rule with the lockfile versus install-time malware split for a package row, the Ubuntu hardening page for sshd or apt, and the secure coding checklist for a handler bug.
A catalog is not a close rule
SOAR as a product is a workflow engine with connectors: take an alert, enrich it, open a case, page someone, maybe fire a containment action. Vendors sell drag-and-drop graphs named after MITRE techniques. Gartner bundled that market as security orchestration, automation, and response. The acronym is the product category. It is not the finding.
A playbook on the sibling page is three fields on one row. ticketId, ownerId, retestId. Close only on a pass, or on written not-affected evidence. That loop can live in GitHub Issues, Jira, or a JSON folder. It does not need a SOAR license. The product can open the row and comment the retest. It cannot invent an owner your CMDB does not have.
NIST SP 800-61 Rev. 2 is still the incident-handling outline I can cite without a vendor blog: prepare, detect and analyze, contain, eradicate, recover, post-incident. NIST CSRC record. That document is about an incident. A weekly scanner CSV is not an incident until someone names the asset and the check. Do not paste a 400-row export into a case and call it orchestration.
Three fields before any seat
Buy nothing until these exist on every accepted finding. The sibling page is the longer template. This page only needs the gate the glue must obey.
- ticketId. One row per asset plus check.
VM-2026-0142is a ticket. A 400-row CSV is not. - ownerId. A person or a pager rotation that can change the package, the image, or the handler.
security-teamis not an owner. - retestId. The same plugin, the same credentials, the same hostname, run after the change. A Slack thumbs-up is not a retest.
CISA issued Binding Operational Directive 26-04 on 10 June 2026. The KEV page now points at that directive. Federal civilian agencies must remediate catalog rows on the listed due dates. Everyone else can still use the catalog as the short list.
A KEV row sets due. It does not assign invoice-oncall. A SOAR watcher that raises the due date is useful after the owner field is already filled. A watcher that only draws a heat map is not.
What the glue is allowed to do
Automation is fine after the three fields exist. The allowed recipe is short on purpose.
- Open
VM-2026-0142from the scanner with the plugin output pasted, not a screenshot. - Map the asset to
ownerIdfrom a file you maintain. If the map is empty, page inventory, not the product engineer. - After the change, rerun the same plugin with the same scan account. Write
retestIdandretestResulton the row. - Refuse close unless
can_closereturns ok. The function lives on the playbook page. Glue must call it, not a dashboard color.
Stop there until last month’s queue is shorter. Then add a KEV watcher that copies the catalog due date. Then add a backport note template for the NovemberWhiskey case. Do not add a malware detonation graph, a GeoIP hop, or a Slack poll until those four steps leave a log on every finding.
Containment actions that change production, such as disabling an account or blocking a token, need a named human on the ticket before the action fires. A recipe that pages nobody and then revokes a session is an outage generator. Keep those steps off until the owner field has been true for a month of findings.
SCANNER plugin + asset + evidence | glue may open the row v TICKET VM-2026-0142 invoice-api-prod | glue may page ownerId v OWNER invoice-oncall ships the image | glue may rerun the plugin v RETEST same plugin, same host pass -> close fail -> reopen SOAR seat optional arrows Playbook the four boxes
An alert becomes a row, or it dies
Incident response and vulnerability management share a tracker. They do not share a due-date rule. A KEV package on that invoice host is a patch clock. A SIEM alert about a failed login burst is an incident candidate. Dumping both into one “SOAR playbook” named after a technique is how neither gets an owner.
Split the inbound webhooks.
# dispatch.py identifiers stay route_alert, Finding
KEV_SOURCE = "cisa-kev"
SCAN_SOURCE = "nessus"
def route_alert(alert):
source = alert.get("source")
if source == SCAN_SOURCE or source == KEV_SOURCE:
return "vm-finding"
if alert.get("sev") in ("SEV1", "SEV2") and alert.get("asset"):
return "incident"
return "drop"
route_alert is the named helper. A scan or a KEV hit becomes a VM finding and must grow the three fields. A high severity with an asset becomes an incident under SP 800-61. Everything else is drop, not a case. A drop is a written rule, not a silent filter you cannot grep.
The SOA / SIR / TIP split is a vendor outline: orchestration, incident response, threat intel. Those are functions. They are not a reason to buy a pane of glass. A TIP that cannot name the invoice host is a feed. A SIR timeline that never writes retestId is a novel.
A dispatch you can actually refuse
Here is VM-2026-0142 after glue has done its job. Identifiers stay the same as the playbook page so the two articles can share a fixture.
finding = {
"ticketId": "VM-2026-0142",
"asset": "invoice-api-prod",
"checkId": "ubuntu_linux_openssl",
"cve": "CVE-2023-0286",
"ownerId": "invoice-oncall",
"openedAt": "2026-08-10T09:12:00Z",
"due": "2026-08-24",
"evidence": "authenticated plugin, OpenSSL before USN backport",
"fix": "apt upgrade openssl to the Jammy USN, rebuild image sha256:b3f1",
"retestId": "nessus-scan-88421",
"retestResult": "not-affected",
"evidenceNote": "dpkg -s openssl shows 1.7, USN names the CVE",
}
kind = route_alert({"source": "nessus", "asset": finding["asset"]})
# kind == "vm-finding"
The named fallback when the scanner still flags 1.7 is already on the sibling page: not-affected plus evidenceNote. Glue must not invent a second closer that keys on green tiles. If you already bought the engine, one recipe is enough: open, assign, retest, close. Delete the rest until those four steps leave a log.
REQUIRED = ("ticketId", "ownerId", "retestId", "retestResult")
def can_dispatch_close(finding):
if route_alert({"source": SCAN_SOURCE, "asset": finding.get("asset")}) != "vm-finding":
return False, "not a vm row"
for key in REQUIRED:
if not finding.get(key):
return False, f"missing {key}"
if finding["retestResult"] not in ("pass", "not-affected"):
return False, "retestResult must be pass or not-affected"
if finding["retestResult"] == "not-affected" and not finding.get("evidenceNote"):
return False, "not-affected needs the package or config proof"
return True, "ok"
can_dispatch_close is the gate the engine calls. It is stricter than a webhook that always comments “done”. A finding that arrived as an incident stays out of this closer. Do not let a SOAR graph close a SEV1 because a package scan later went green.
Prove the seat is not the loop
You are proving your tracker and, if you have one, your engine. You are not walking a stranger’s SOC.
- Pick one open row on
invoice-api-prod. Confirm it hasticketId,ownerId, and pasted plugin output. If any field is empty, stop. Do not open the SOAR editor. - If you have glue, trigger only the open-and-assign recipe on a finding you already own. Expect a page to
invoice-oncall, not toappsec. - After the change, rerun the same plugin. Save the job id as
retestId. - Run
can_dispatch_closeagainst the JSON. Expect a refusal untilretestResultispassor a documentednot-affected. - Count recipes in the catalog that have not fired in 30 days. Those are slides. Disable them.
python3 - <<'PY'
from dispatch import can_dispatch_close, route_alert
import json, pathlib
print(route_alert({"source": "nessus", "asset": "invoice-api-prod"}))
# asset taken from the VM-2026-0142 fixture
for path in pathlib.Path("tickets").glob("VM-*.json"):
finding = json.loads(path.read_text())
ok, reason = can_dispatch_close(finding)
print(finding["ticketId"], ok, reason)
PY
# Expect: vm-finding
# Expect: VM-2026-0142 True ok
# Expect: any closed row without retestId -> False missing retestId
Reserved concurrency, WAF rules, and a vendor patch agent are not this page. A patch agent was a common 2021 closer. A patch agent can be the change that invoice-oncall ships. It is not a substitute for retestId. If you already run one, keep it under the same gate.
Questions we keep getting
Do we need a SOAR product to run the close loop?
No. A GitHub issue, a YAML owner map, and a script that refuses close are enough. Glue is optional after those exist. The sibling playbook page is the loop. This page is the seat you may skip.
What happened to the 2019 30 percent prediction?
I could not find a first-party 2026 Gartner number that replaces it. Treat the 2019 Market Guide as history. Do not use it as a 2026 buying reason.
Can one recipe cover both KEV patches and SEV1 incidents?
No. route_alert splits them. A catalog due date is a patch clock. A SEV1 is an incident under SP 800-61. Sharing a graph hides the owner on both.



