A Phishing-Response Playbook in 12 Steps: End-to-End in Krasper Suite
A concrete, step-by-step walkthrough of how a real phishing-response playbook is wired together end-to-end in Krasper Suite, from alert ingress to audit record. Twelve nodes, three conditional branches, one dry-run gate before anything touches production.

TL;DR: The previous post in this series argued for the four design choices that keep SOAR playbooks alive: reactive data flow, typed node contracts, first-class branching, dry-run. This one shows what those choices look like in a working playbook. Twelve nodes, walked one at a time, with the data shape, the failure mode, and the design intent for each. By the end, the playbook handles the long tail of real phishing variation that scripted automations cave under.
Phishing remains the highest-volume initial-access vector in most organizations. It is also the incident category where the cost of poor automation is most visible: a single broken playbook can either miss real threats or, worse, take destructive action on a false positive (quarantining a legitimate CEO email, isolating a sales laptop mid-deal call). Doing it well is mostly about disciplined wiring; the logic itself is rarely the hard part.
This walkthrough follows one playbook end-to-end as it lives in Krasper Suite's designer. You can adapt the exact shape to any environment; the point of the walkthrough is the discipline it demonstrates.
Contents
- Ingress: alerts arriving from the mail-security signal
- Header parsing and message decomposition
- IOC extraction: URLs, domains, hashes, sender
- Threat-intelligence enrichment
- Sender reputation and domain-age signals
- Recipient resolution and asset mapping
- Phishing-confidence classification (the central switch)
- High-confidence branch: quarantine across mailboxes
- URL-click detection on affected endpoints
- Endpoint isolation (with explicit failure handling)
- Multi-channel notification with role-aware routing
- Ticket creation, audit trail, and post-mortem record
Plus: dry-run discipline, what the screenshots actually show, and the one branch most teams forget to wire.
Step 1: Ingress
The first node receives alerts from the mail-security signal. In the designer, this is a single input port with a declared schema: the alert object as published onto the shared event bus by the mail gateway adapter.
┌──────────────────────────────────┐ │ Alert ingress │ │ │ │ in: (event bus subscription) │ │ out: alert: MailAlert │ └──────────────────────────────────┘
[Screenshot: alert-ingress node selected in the playbook canvas, side panel showing the MailAlert schema]
What matters here is that the alert is already typed. There is no "parse this JSON" step at the front of the playbook. That responsibility belongs to the mail-gateway adapter, which guarantees the shape before the alert ever reaches the bus. Authors of the playbook write against a contract, not against raw payloads.
If the upstream adapter is unavailable, no alert reaches this node; no execution starts; nothing fails silently. The platform's health view is the place to discover that, not the playbook itself.
Step 2: Header parsing and message decomposition
The first transformation node decomposes the mail object into the fields downstream nodes will need: headers, subject, body_text, body_html, attachments[]. This is a pure function: no external calls, no side effects, no failure modes beyond malformed input (which routes to the error port and triggers an analyst alert).
┌──────────────────────────────────┐ │ Decompose message │ │ │ │ in: alert: MailAlert │ │ out: message: ParsedMail │ │ err: ParseError → analyst │ └──────────────────────────────────┘
The reason this is its own node rather than inlined into Step 3 is testability. The parser has its own test suite. When the mail gateway upstream changes a field name, exactly one node breaks, with exactly one fix, reviewable in isolation.
Step 3: IOC extraction
This node extracts indicators of compromise from the parsed message: URLs in the body, domains derived from those URLs, hashes for any attachments, the sender address, the reply-to address if it differs.
┌──────────────────────────────────┐
│ Extract IOCs │
│ │
│ in: message: ParsedMail │
│ out: iocs: IOCSet { │
│ urls[], domains[], │
│ hashes[], senders[] │
│ } │
└──────────────────────────────────┘
[Screenshot: extracted IOC list in the dry-run inspector, showing the urls/domains/hashes arrays as resolved at runtime]
Two design notes that matter at scale.
First, URL extraction normalizes before deduplication. http://x.com, https://x.com/, and https://X.COM collapse to a single canonical URL. Without this, downstream lookups duplicate cost and the classifier double-counts the same indicator.
Second, attachment hashing uses SHA-256 only. Older hash algorithms add noise without value; the threat-intelligence APIs the next step calls accept SHA-256 universally. One hash family, one code path, one less source of integration drift.
Step 4: Threat-intelligence enrichment
The next node is the first one that calls external infrastructure. The IOC set fans out to a threat-intelligence adapter (typically a service like VirusTotal or an equivalent), which returns a verdict per indicator: known-malicious, known-clean, unknown.
┌──────────────────────────────────┐ │ Threat-intel lookup │ │ │ │ in: iocs: IOCSet │ │ out: verdicts: VerdictMap │ │ err: TIError → degraded path │ │ timeout: 8s │ └──────────────────────────────────┘
Three platform guarantees apply here automatically: the call carries an idempotency key (so a replay does not duplicate API quota), the timeout is bounded (so a slow upstream cannot stall the entire execution), and the error port is mandatory (so an outage at the threat-intel provider does not silently downgrade the playbook to "every IOC is unknown").
The error port routes to a degraded path rather than to a hard abort. If the threat-intel API is unavailable, the playbook still runs. It just classifies with the signals it has (sender reputation, domain age, mail headers) and flags the verdict as intel_unavailable in the audit record. The analyst seeing the resulting ticket knows the confidence is lower because the enrichment was incomplete.
One more nuance worth wiring: results from this node are cached for a bounded window (typically a few hours) keyed on the indicator itself, not the alert. A phishing campaign that lands a hundred near-identical messages in the same morning should not produce a hundred billable lookups for the same URL. The cache layer is part of the adapter, not the playbook, so authors do not have to think about it, but knowing it exists is the difference between an automation that scales economically and one that quietly burns through the threat-intel API quota by the second week.
Step 5: Sender reputation and domain age
Parallel to the threat-intel call, two cheaper enrichments run: the sender domain's reputation (from a separate adapter) and the registration age of the sender domain (via WHOIS-style lookup). The runtime parallelizes these because they have no dependency on each other or on the threat-intel result.
┌────────────────────────┐ ┌────────────────────────┐ │ Sender reputation │ │ Domain age │ │ │ │ │ │ in: iocs.senders │ │ in: iocs.domains │ │ out: rep_score │ │ out: age_days │ └────────────────────────┘ └────────────────────────┘
Domain age matters because freshly registered domains correlate strongly with phishing campaigns. A domain that was registered three days ago and is asking your CFO to wire money deserves more scrutiny than one registered fifteen years ago. The age signal alone is not enough; combined with the other signals it raises confidence considerably.
Step 6: Recipient resolution and asset mapping
The mail alert lists recipients by email address. Containment decisions need more: which user, what role, which endpoint, where they sit in the org. This node resolves each recipient against the shared canonical asset model.
┌──────────────────────────────────┐
│ Resolve recipients │
│ │
│ in: message.recipients[] │
│ out: targets: ResolvedTarget[] { │
│ user_id, role, │
│ endpoints[], │
│ privileged: bool │
│ } │
└──────────────────────────────────┘
[Screenshot: resolved targets in the dry-run inspector, each recipient mapped to user_id, role, endpoints, privileged flag]
The privileged flag is a derived field, true if the user is in any group that warrants elevated containment (finance approvers, executive assistants, IT administrators). The playbook treats privileged users differently in Step 11, so this resolution has to happen before the central switch.
If a recipient cannot be resolved (external recipient, freshly provisioned user not yet in the asset model), the target is included in the array with resolved: false rather than dropped. Downstream nodes can filter on resolution status; nothing silently disappears.
Step 7: Phishing-confidence classification
This is the central decision point. A classifier node takes every enrichment signal and emits a confidence: high | medium | low verdict, plus the structured reasoning behind the verdict.
┌──────────────────────────────────────────────────────┐
│ Classify confidence │
│ │
│ in: verdicts, rep_score, age_days, targets, message │
│ out: classification: { │
│ confidence: "high" | "medium" | "low", │
│ signals: [...], │
│ intel_complete: bool │
│ } │
└──────────────────────────────────────────────────────┘
│
▼
┌────────────────────────────┐
│ switch confidence │
│ case high ──▶ Step 8 │
│ case medium ──▶ analyst │
│ case low ──▶ trend tag │
│ default ──▶ analyst │
└────────────────────────────┘
The classifier itself is deliberately explainable. It is a weighted rule set, and the signals that contributed to each verdict are recorded in the output, so nothing hides behind a black-box ML score. When the analyst opens the resulting ticket, they see why the playbook classified the alert the way it did. That auditability matters as much as the accuracy: a high-confidence verdict the analyst cannot trace back to its inputs erodes trust in the entire automation.
Concretely, the signal weights are version-controlled alongside the playbook itself. A change to "treat newly-registered domains as higher risk" is a reviewable diff, not a configuration drift in a production database. When the audit asks "why did this alert get quarantined on March 14," the answer is a specific commit hash with the classifier weights at the time of execution, recorded in the audit record.
The default branch is wired, even though every confidence value should be one of the three cases. This is the most common "we forgot it" branch, and the one that protects against future classifier changes that introduce a fourth case nobody updated the switch for. If the classifier evolves to emit confidence: "requires-human-review" six months from now, the playbook routes to the analyst by default instead of failing closed in a way that silently drops the alert.
Step 8: Quarantine across mailboxes (high-confidence branch)
In the high-confidence branch, the first action is to quarantine the message across every mailbox it reached. The quarantine node calls the mail-platform adapter, idempotent on the (campaign_id, message_id) pair so re-execution does not duplicate effects.
┌──────────────────────────────────┐ │ Quarantine mail │ │ │ │ in: message.id, targets │ │ out: quarantine_result │ │ err: → escalate to analyst │ │ dry-run: synthetic success │ └──────────────────────────────────┘
[Screenshot: dry-run trace showing the quarantine node producing a synthetic success and the recorded "would have quarantined N messages" intent]
This is the first destructive node in the playbook. The dry-run discipline matters most here: every modification to this branch is tested against the dry-run corpus before merge, because a regression that quarantines legitimate mail at scale is a production incident of its own.
Step 9: URL-click detection
In parallel with the quarantine, the playbook checks whether any recipient already clicked any of the malicious URLs. This is a query against the endpoint telemetry (proxy logs, browser telemetry, endpoint agent) for the affected users in the window between the mail's arrival and the playbook's execution.
┌──────────────────────────────────┐ │ URL-click detection │ │ │ │ in: iocs.urls, targets, │ │ message.received_at │ │ out: clicks: ClickEvent[] │ └──────────────────────────────────┘
If the array is empty, the playbook continues to notification. If it contains entries, the next step escalates to endpoint containment for the affected hosts.
This step only reads; it produces no destructive output and raises no idempotency concern. It is the kind of node that authors sometimes inline into a larger combined step, and that is worth resisting. Pure query nodes belong in their own boxes because they are the easiest to reuse across other playbooks.
Step 10: Endpoint isolation (with explicit failure handling)
For each endpoint where a click was detected, the playbook calls the endpoint adapter to isolate it from the network. This step is the one most likely to fail in real conditions: the endpoint may be offline, the agent may be unreachable, the adapter may be rate-limited.
┌──────────────────────────────────┐ │ Isolate endpoint (for each) │ │ │ │ in: endpoint_id │ │ out: isolation_result │ │ err: IsolationFailed → │ │ force-priority alert │ │ timeout: 30s │ │ dry-run: synthetic success │ └──────────────────────────────────┘
The error port here does not route to "log and continue." It routes to a force-priority alert to the on-call analyst with the endpoint context attached, because a failed containment on a known compromise is a higher-urgency event than the original alert. The playbook does not pretend a failed isolation succeeded; it escalates explicitly, and the audit record reflects both the attempt and its outcome.
Step 11: Multi-channel notification with role-aware routing
Affected users need to be notified, but the channel and tone depend on the user. The notification node fans out per-target with role-aware routing:
- Standard users: notification through the approved internal channel (mail or chat), explaining what happened and what to do.
- Privileged users (finance, executive, IT admins): same notification plus an immediate force-priority alert through the on-call channel, because the consequence of credential compromise is higher.
- External recipients: no automatic notification, escalated to a human for review.
┌────────────────────────────────────────────────┐ │ Notify (for each target) │ │ │ │ in: target, message, classification │ │ out: notify_result │ │ err: NotifyFailed → analyst │ │ dry-run: synthetic success │ │ │ │ routing: │ │ target.privileged == true → on-call + user │ │ target.privileged == false → user only │ │ target.resolved == false → analyst review │ └────────────────────────────────────────────────┘
The notification text is not generated at runtime by an LLM, at least not in the destructive branch. Templates are pre-approved, versioned, and rendered with the alert-specific fields injected. This keeps the notification auditable and consistent, and it prevents a model regression from producing alarming or confusing messages to non-technical users at scale.
Step 12: Ticket creation, audit trail, and post-mortem record
The final node creates the incident ticket and writes the post-mortem record. The ticket includes the full classification (with contributing signals), the action trace (what was quarantined, what was isolated, who was notified), and links to the raw alert and the playbook execution trace.
┌──────────────────────────────────┐ │ Create incident + audit record │ │ │ │ in: classification, actions, │ │ targets, execution_trace │ │ out: incident_id │ │ err: → emergency mail to SOC │ │ dry-run: synthetic success │ └──────────────────────────────────┘
[Screenshot: ticket detail view showing the classification signals, the action trace, and the link back to the playbook execution]
The execution trace is the part most teams underinvest in. Reconstructing what the playbook did, six weeks later when the audit is happening, requires every node's input, output, branch decision, and external call to be persisted with a stable execution ID. Without that discipline the playbook is effectively a black box; with it, auditors can follow the automation node by node, which is what makes it defensible.
Dry-run discipline before any of this ships
None of the twelve steps above ships without passing dry-run against a curated alert corpus. The corpus contains: a representative high-confidence phishing alert with privileged recipients, a medium-confidence alert with mixed recipients, a low-confidence alert, an alert where threat-intel times out, and an alert with an unresolvable external recipient. Every change to the playbook runs against all five in CI; any divergence from the expected execution trace fails the build.
This is the difference between a playbook the team trusts and one the team works around. The branches above are not theoretical: they are the cases the corpus exercises on every change.
The one branch most teams forget to wire
Re-read Step 7. The default branch on the central switch routes to the analyst. Re-read Step 10. The error port on isolation routes to a force-priority alert. Re-read Step 4. The error port on threat-intel routes to a degraded path that still completes the playbook.
Across all three, the failure mode is wired, named, and audited. That is what separates an automation operators trust from one they quietly bypass after the first 3 AM page where the playbook did nothing. It costs you more nodes on the canvas and buys you a playbook that actually runs unattended.
Closing
That is the shape of the whole thing: twelve nodes, three conditional branches, and four explicit error paths, all gated behind a single dry-run check before anything reaches production. This is what end-to-end phishing response looks like once you take the wiring discipline seriously.
The next post in this series steps up one altitude, from a single playbook to the metrics that tell you whether your playbooks, in aggregate, are moving the needle. We will look at coverage, time-to-contain, false-positive rate and automation share, the numbers a CISO cares about, and at how to instrument them.
Further reading
- NIST SP 800-61r3: Computer Security Incident Handling Guide
- MITRE ATT&CK: Initial Access: Phishing (T1566)
- ENISA: Phishing Threat Landscape annual report
- CISA: Stop Ransomware guidance on initial-access defense
enterprise infrastructure?
Schedule a technical briefing. No sales pitch, just architects and your team.