SOAR & response

Playbooks that automate first response.

Advanced30 min · lesson 6 of 15

A hospital does not improvise when a patient's heart stops. Someone calls a "code blue" and a rehearsed routine takes over: one person starts chest compressions, one wheels in the crash cart, one writes down every time and every dose. The speed comes from the routine, not from anyone being a hero. SOAR (Security Orchestration, Automation and Response) is the code-blue routine for your SOC (Security Operations Centre, the team watching the alerts). A detection fires, an agreed workflow runs, and it runs the same way at 3 a.m. as it does at 3 p.m. The analyst who picks the alert up walks into an organised scene instead of a bare one-line message.

Take the acronym apart, because each word is a separate capability. *Orchestration* is wiring your tools together: the SIEM (Security Information and Event Management platform, the system that gathers logs and raises alerts), the identity provider, the EDR (Endpoint Detection and Response agent sitting on laptops and servers), the ticket system, the chat tool, all reachable through their APIs (Application Programming Interfaces, the machine-to-machine doors a product exposes) so they behave like one system. *Automation* is the steps a machine can run without you: enrich this IP address, pull this user's last twenty sign-ins. *Response* is the part with teeth in the real world: revoke sessions, isolate a laptop, disable an account. Riding along with those three is *case management*, a durable workspace (TheHive, Jira, or the SOAR's own) where evidence and decisions get written down. The unit of work is the *playbook*: a codified workflow, kept under version control, started by an alert. It is the response-as-code sibling of detection as code.

What a SOAR engine actually does

Scrape off the vendor gloss and a SOAR engine is a workflow runner with a credential safe bolted on. A *trigger* starts an *execution*. Usually that trigger is a webhook your SIEM calls; sometimes it is a poll or a schedule. The workflow is a graph of nodes, and each node is an "app", which sounds exotic and is not. An app is an authenticated API client. The Okta app wraps Okta's REST API, the Defender app wraps the machine-actions API, and that is the whole trick. JSON (JavaScript Object Notation, the text format tools use to hand structured data to each other) falls out of one node and becomes template input to the next, written like {{ alert.user }}. Decision nodes branch on field values. Every execution is written down step by step. That audit trail earns its keep the day a playbook disables a vice president's account, because you have to be able to show exactly which input produced that decision.

The money is why any of this exists. A mid-size SOC handles thousands of alerts a day, and triaging one by hand costs 15 to 30 minutes: look up the user, look up the asset, check the IP reputation, decide, raise a ticket. Enrichment is nothing but API calls, so automating it collapses *MTTA* (mean time to acknowledge, how long before a human even looks at the alert) and moves analyst minutes off copy-paste and onto judgement. Containment automation goes after *MTTR* (mean time to respond, how long until the bleeding stops). Revoking a stolen session in 40 seconds instead of 40 minutes is often the whole difference between one compromised account and a tenant-wide incident.

Anatomy of a playbook

Every playbook worth keeping has the same five parts. A trigger with a tight filter, keyed on named rules rather than "anything critical". Enrichment, run in parallel, because those lookups do not depend on each other. A decision gate that weighs how confident the detection is against *blast radius*, meaning how much damage the action does if the alert turns out to be wrong. Actions, ordered so the cheapest thing to undo happens first (revoke sessions before you disable the account). And case creation that attaches everything the playbook found out. Here is that contract written as YAML (YAML Ain't Markup Language, a plain-text config format), the same graph Shuffle or Cortex XSOAR keeps in its own format:

playbook.yml
# Response contract for one specific detection — not a generic catch-all
trigger:
source: siem.webhook
filter: alert.rule == "impossible-travel" && alert.severity >= high
steps:
- id: enrich # parallel: the calls are independent
parallel:
- identity.get_recent_signins: { user: "{{ alert.user }}" }
- cmdb.get_asset_owner: { host: "{{ alert.host }}" }
- intel.check_ip: { ip: "{{ alert.src_ip }}" }
- id: gate # confidence x blast radius decides the path
if: enrich.intel.confidence >= 90 && !alert.user.is_break_glass
then: contain
else: approve # one-click Slack approval, 15 min timeout
- id: contain # cheapest-to-reverse action first
actions:
- idp.revoke_sessions: { user: "{{ alert.user }}" }
- idp.disable_user: { user: "{{ alert.user }}" }
- id: case
thehive.create_alert: { title: "{{ alert.rule }} — {{ alert.user }}",
attach: [enrich.*, contain.*] }
pagerduty.trigger: { urgency: high }

Treat this file the way you treat detection code. It lives in Git, changes go through review, merges deploy through the SOAR's API. The gate step is where the design actually lives. is_break_glass keeps emergency-access accounts, the ones you reach for when everything else is locked out, off the auto-disable path. The approve branch turns a risky automation into a one-click human decision, which buys you most of the speed with none of the unilateral risk.

Stand up a SOAR and fire a test alert

You can run this whole pattern on a laptop with Shuffle, an open-source SOAR. Deploy it, build a workflow with a webhook trigger, then play the part of the SIEM yourself with curl. That is exactly what Splunk or Sentinel does in production, only their payload is signed:

deploy-and-trigger.sh
# Self-hosted SOAR in a few minutes (needs Docker, Git, ~4 GB RAM)
git clone https://github.com/Shuffle/Shuffle && cd Shuffle
# Linux prereqs from the install guide — OpenSearch needs both:
mkdir -p shuffle-database && sudo chown -R 1000:1000 shuffle-database
sudo sysctl -w vm.max_map_count=262144
docker compose up -d
# UI at http://localhost:3001 — create a workflow, add a Webhook
# trigger, copy its URL, then play the SIEM's role:
curl -s -X POST \
"http://localhost:3001/api/v1/hooks/webhook_3d8e1c72-b2f4-4d1e-9a77-0f2c6a9e4b10" \
-H "Content-Type: application/json" \
-d '{"rule":"impossible-travel","severity":"high",
"user":"jdoe","src_ip":"185.220.101.34","host":"FIN-LT-0042"}'
# {"success": true, "execution_id": "7f0a41c2-58bb-4a55-a1d7-1e2f0a9c33d1"}
# Each run is a logged execution — every node's input/output captured:
docker compose logs --tail 50 backend | grep -i execut
# ... [INFO] Handling webhook execution for workflow impossible-travel-response
# ... [INFO] Execution 7f0a41c2-58bb-4a55-a1d7-1e2f0a9c33d1 is FINISHED (6/6 actions)

One POST (the HTTP verb for sending data to a server) created an execution, fanned out the enrichment nodes, evaluated the gate and recorded the lot. Practice the failure paths now, while nothing is on fire. Send a payload with a missing field. Send one naming a user who does not exist. Send a malformed IP address. Then confirm the workflow fails *safe*, meaning it opens a case and pages a human, rather than dying quietly.

Enrichment lands in a case, not a chat message

Enrichment that scrolls past in a Slack channel is wasted work. It has to land somewhere durable and searchable. TheHive is the case manager most open-source SOCs settle on. It was born open source, though TheHive 5 now ships under StrangeBee's free Community licence rather than an open-source one. Cortex is its analysis engine: *analyzers* wrap intel lookups (VirusTotal, AbuseIPDB, Shodan) and *responders* wrap actions. The final step of your playbook creates the alert through the API, with observables typed properly so you can still pivot on them weeks later:

create-case.sh
curl -s -X POST "http://thehive.local:9000/api/v1/alert" \
-H "Authorization: Bearer $THEHIVE_KEY" \
-H "Content-Type: application/json" \
-d '{
"type": "siem", "source": "sentinel", "sourceRef": "inc-4211",
"title": "Impossible travel — jdoe (Oslo -> Jakarta, 41 min)",
"description": "Two sign-ins ~10,900 km apart. Sessions revoked by PB-114.",
"severity": 3,
"observables": [{"dataType": "ip", "data": "185.220.101.34"}]
}' | jq '{id: ._id, status: .status, severity: .severity}'
# {
# "id": "~40968312",
# "status": "New",
# "severity": 3
# }

The _id that comes back is the handle for everything downstream. Later playbook steps attach artifacts to it, the analyst promotes the alert into a full case, and your programme metrics read closure data out of it. One rule keeps the roles clean: the playbook *writes to* the case, humans *decide in* it.

Containment is the sharp end

Containment is where blast radius stops being an abstraction, so learn the primitives exactly. In Microsoft Defender for Endpoint, isolating a machine is an asynchronous *machine action*. You POST an isolate request, the API answers Pending, and the agent enforces it at its next check-in. Selective isolation leaves Outlook and Teams working, which is often the right default. A user who can still message you to ask why their laptop is locked starts fewer parallel fires than one who has gone completely dark:

isolate-host.sh
# App registration needs the Machine.Isolate API permission
curl -s -X POST \
"https://api.security.microsoft.com/api/machines/$MACHINE_ID/isolate" \
-H "Authorization: Bearer $MDE_TOKEN" \
-H "Content-Type: application/json" \
-d '{"Comment":"PB-114 impossible-travel: token replay confirmed",
"IsolationType":"Selective"}' | jq '{id, type, status}'
# {
# "id": "b2a7f3d1-4c8e-4f6a-9d21-8e5c0a1b7f44",
# "type": "Isolate",
# "status": "Pending"
# }
# Containment is async — poll the machine action until the agent acks:
curl -s "https://api.security.microsoft.com/api/machineactions/b2a7f3d1-4c8e-4f6a-9d21-8e5c0a1b7f44" \
-H "Authorization: Bearer $MDE_TOKEN" | jq .status
# "Succeeded"

CrowdStrike does the same job in one PSFalcon line, Invoke-FalconHostAction -Name contain -Id <aid>, and every EDR has an equivalent. The policy question is identical whatever the vendor: which detections have *earned* the right to pull these levers with no human in the loop? A workable rule is to automate when the rule's precision has been measured and proven and the action is cheap to reverse. Session revocation on high-confidence token theft, automate it. Disabling the service account that runs payroll, approval gate, always. And make your actions *idempotent*, so re-running a playbook against a host that is already isolated does nothing at all. Retries happen.

Auto-containment on a noisy rule is an outage you built yourself
Here is a real failure mode. A geolocation database update reclassifies your VPN (Virtual Private Network) egress range, "impossible travel" fires for an entire office, and the playbook obediently disables 300 accounts before the on-call phone rings. Wire high-impact actions only to detections with a measured false-positive history. Cap actions-per-hour per playbook. Always exclude break-glass accounts. And treat the SOAR itself as a tier-0 target: it holds credentials that can disable users and isolate hosts, so scope its API tokens to the bare minimum and audit access to it the way you would audit a domain controller.

Trade-offs, cost, and what SOAR can't do

The costs are real. *Connector rot* is the tax you pay every day. Vendors version their APIs, tokens expire, fields get renamed, and a playbook that ran clean for six months quietly stops working. Monitor playbook executions the way you monitor production services, with alerting on failure rate. Pricing shapes architecture too. Microsoft Sentinel's playbooks are Azure Logic Apps, billed per action execution, so a chatty enrichment loop turns into a literal invoice. Hosted platforms like Tines and Torq meter usage in tiers. Self-hosted Shuffle swaps licence spend for your own maintenance hours. *Playbook sprawl* is the governance version of the same bill: fifty overlapping playbooks nobody owns is a worse place to be than ten reviewed ones.

Know the limits as well. SOAR runs the response you already designed; it cannot work out which response is right. Garbage detections in, automated garbage out, which is why this lesson stands on top of alert quality instead of replacing it. And it only ever reacts to rules that fired.

The gate: confidence x blast radius decides the path
Gate (after enrichment)
the design centre of every playbook: confidence x blast radius
confidence >= 90 AND not break-glass
Auto-contain
revoke sessions first (cheapest to reverse), then disable/isolate - no human in the loop
below threshold or high blast radius
One-click approval
route to Slack, 15-min timeout - most of the speed, none of the unilateral risk
break-glass / emergency-access account
Excluded from automation
is_break_glass accounts must never be auto-disabled
The gate is a dial. As a detection's measured precision climbs, its actions move from the approval branch to the automatic one.

That last limit is the one to sit with. Automation handles the intrusions your rules already describe. The attacker who never trips a rule sails past every playbook you own. Finding *that* intrusion means going looking on purpose: form a hypothesis about how an adversary would move through your environment, then interrogate the data to prove it or kill it. That discipline is hypothesis-driven hunting, and it is where the course turns next.

Try this

Dry-run the enrichment-only path. Given an alert ID, pull the related entities and open a case with no containment attached. Then check the case fields against what your analysts actually read.

terminal
# Pseudocode / API sketch — replace with your SOAR or SIEM case API
$ ALERT_ID=alert-18421
$ curl -s -H "Authorization: Bearer $SOAR_TOKEN" \
"$SOAR_URL/api/alerts/$ALERT_ID" | jq '{id, severity, entities}'
{
"id": "alert-18421",
"severity": "high",
"entities": [{"type":"user","value":"j.alvarez"},{"type":"ip","value":"203.0.113.44"}]
}
$ curl -s -X POST -H "Authorization: Bearer $SOAR_TOKEN" \
-H "Content-Type: application/json" \
"$SOAR_URL/api/cases" -d '{"alert_id":"alert-18421","playbook":"enrich-only-v3"}' \
| jq '{case_id, status, actions_run}'
{
"case_id": "case-9921",
"status": "enriched",
"actions_run": ["geoip","user_risk","related_signins"]
}
# No isolate/disable in this dry-run — on purpose.

Takeaway

Automate the chores first and the judgement last. Enrichment is a chore. Deciding whether to lock a human being out of their own account is not, so it stays behind a gate until the rule driving it has earned its way out.

Next step: put one enrichment playbook under version control, attach it to a single high-fidelity detection, and add an audit field recording the playbook version before you switch on anything destructive.

Quick check
01Your SOAR playbook can either fire a containment action on its own or hand it to a one-click human approval gate. By this lesson's rule, which action has genuinely earned the right to run fully automatically?
Correct — The lesson's rule is to automate when the rule's precision is measured and proven AND the action is cheap to reverse. Session revocation on high-confidence token theft clears both bars.
Incorrect — No. The lesson names this exact case as an approval gate, always, because the blast radius is huge even when the alert is correct.
Incorrect — No. That is the self-inflicted outage in the warning: a geolocation database update reclassifies the VPN egress and the playbook disables 300 accounts before the on-call wakes up.
Incorrect — No. The lesson warns against keying automation on "anything critical". Automation belongs to a specific rule with a measured false-positive history, not to a broad severity band.
02Your playbook POSTs an isolate request to Microsoft Defender for Endpoint and the API comes back with "status": "Pending". What has actually happened at that moment?
Correct — Isolation is an asynchronous machine action. Pending means accepted, not enforced, which is why the lesson polls /api/machineactions/<id> until the status flips to Succeeded.
Incorrect — No. Pending is the opposite of a confirmation. The host stays on the network until the agent checks in and acknowledges the action.
Incorrect — No. A missing permission fails the call outright. Pending is the normal first answer for a machine action that Defender has accepted.
Incorrect — No. Nothing in that response changes the isolation type you asked for. Pending is about timing, not about which kind of isolation runs.
03It is 06:40. Your impossible-travel playbook has run 180 times in eleven minutes, every alert is for staff in the same office, every execution shows the same VPN egress IP, and twelve accounts are already disabled. What does this lesson tell you to do?
Correct — This is the geolocation-update failure the warning describes. The rule's precision has collapsed, so its actions lose the right to run unattended until the rule is proven again.
Incorrect — No. Alert volume is not evidence and it is not a success metric. One shared egress IP for one office points at a data problem inside the rule, not at 180 separate attackers.
Incorrect — No. Severity is a label on the alert. Changing it does nothing about the wrong containment actions the playbook is still taking every few seconds.
Incorrect — No. Idempotency means a re-run against an already-contained target is a no-op, so this changes nothing. The problem is that the destructive branch should not be running at all right now.

Related