SOAR & response
Playbooks that automate first response.
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:
# Response contract for one specific detection — not a generic catch-alltrigger:source: siem.webhookfilter: alert.rule == "impossible-travel" && alert.severity >= highsteps:- id: enrich # parallel: the calls are independentparallel:- 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 pathif: enrich.intel.confidence >= 90 && !alert.user.is_break_glassthen: containelse: approve # one-click Slack approval, 15 min timeout- id: contain # cheapest-to-reverse action firstactions:- idp.revoke_sessions: { user: "{{ alert.user }}" }- idp.disable_user: { user: "{{ alert.user }}" }- id: casethehive.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:
# 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-databasesudo sysctl -w vm.max_map_count=262144docker 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:
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:
# App registration needs the Machine.Isolate API permissioncurl -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.
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.
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.
# 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.
"status": "Pending". What has actually happened at that moment?Pending means accepted, not enforced, which is why the lesson polls /api/machineactions/<id> until the status flips to Succeeded.Pending is the opposite of a confirmation. The host stays on the network until the agent checks in and acknowledges the action.Pending is the normal first answer for a machine action that Defender has accepted.Pending is about timing, not about which kind of isolation runs.