Featured image of post SOAR Playbooks: Phishing & Compromised Account Response

SOAR Playbooks: Phishing & Compromised Account Response

Production-ready SOAR playbooks for phishing response and compromised account containment — with full step-by-step logic, decision trees, and integration pseudocode.

Overview

A SOAR playbook is only as good as its logic. This page documents two production playbooks:

  1. Phishing Response — from reported email to full remediation
  2. Compromised Account — from anomalous login to account lockdown

Both are built to run on any SOAR platform (Palo Alto XSOAR, Splunk SOAR, Microsoft Sentinel Playbooks) with minor syntax adjustments. The logic and decision trees are platform-agnostic.


Playbook 1: Phishing Response

Decision Tree

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Alert: Phishing Reported
   Parse email headers
     ┌────┴────┐
   SPF fail?  DKIM missing?
     └────┬────┘
          │ YES (either)
   Extract URLs + attachments
   Enrich all IOCs (VT, URLScan)
     ┌────┴──────────────┐
  Any IOC      No malicious
  malicious?      IOCs?
     │               │
     ▼               ▼
  MALICIOUS      SUSPICIOUS or CLEAN
  branch            branch
     │               │
     ▼               ▼
Purge emails    Notify analyst
Isolate if      for manual review
endpoint hit
Create P1 ticket

Step-by-Step Logic

Step 1 — Trigger

The playbook fires when:

  • A user submits a phishing report via the “Report Phishing” button
  • The email security gateway (Proofpoint/Defender) quarantines a message and creates a SOAR case
  • A SIEM alert fires on a known phishing signature

Step 2 — Header Analysis

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Pseudocode — adapt to your SOAR's scripting syntax
headers = get_email_headers(case.email_id)

indicators = {
    'spf_fail':            'fail' in headers.spf.lower(),
    'dkim_missing':        headers.dkim is None,
    'from_reply_mismatch': headers.from_addr != headers.reply_to,
    'suspicious_subject':  any(kw in headers.subject.lower()
                               for kw in ['urgent', 'action required',
                                          'verify', 'suspended', 'invoice']),
}

header_score = sum([
    20 if indicators['spf_fail'] else 0,
    15 if indicators['dkim_missing'] else 0,
    15 if indicators['from_reply_mismatch'] else 0,
    10 if indicators['suspicious_subject'] else 0,
])

Step 3 — IOC Extraction and Enrichment

1
2
3
4
5
6
7
8
urls        = extract_urls(case.email_body)
attachments = extract_attachments(case.email_id)

url_results  = [virustotal_check_url(u)  for u in urls]
hash_results = [virustotal_check_hash(a.sha256) for a in attachments]

malicious_urls  = [r for r in url_results  if r.malicious_count > 3]
malicious_files = [r for r in hash_results if r.malicious_count > 5]

Step 4 — Verdict Branch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
total_score = header_score
total_score += 30 * len(malicious_urls)
total_score += 40 * len(malicious_files)

if total_score >= 60:
    verdict = 'MALICIOUS'
elif total_score >= 25:
    verdict = 'SUSPICIOUS'
else:
    verdict = 'CLEAN'

Step 5a — MALICIOUS branch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
if verdict == 'MALICIOUS':
    # 1. Purge email from all mailboxes
    defender_purge_email(
        sender=headers.from_addr,
        subject=headers.subject,
        scope='all_mailboxes'
    )

    # 2. Block sender domain on email gateway
    email_gateway_block_sender(domain=extract_domain(headers.from_addr))

    # 3. Check if any endpoint executed an attachment
    for att in attachments:
        endpoint_hits = crowdstrike_search_hash(att.sha256)
        if endpoint_hits:
            for host in endpoint_hits:
                crowdstrike_contain_host(host.device_id)
                case.add_note(f"Host {host.hostname} contained — hash match on {att.filename}")

    # 4. Block malicious URLs at proxy/firewall
    for url in malicious_urls:
        firewall_block_url(url.value)

    # 5. Push IOCs to MISP
    misp_create_event(
        title=f"Phishing Campaign — {headers.from_addr}",
        iocs=malicious_urls + malicious_files,
        tlp='amber'
    )

    # 6. Create P1 incident ticket
    ticket = servicenow_create_incident(
        priority=1,
        short_desc=f"Phishing: Malicious email from {headers.from_addr}",
        description=case.build_report()
    )

    # 7. Notify SOC channel
    teams_notify(
        channel='#soc-alerts',
        message=f"[P1] Phishing confirmed. Emails purged. "
                f"{len(endpoint_hits)} endpoints contained. "
                f"Ticket: {ticket.number}"
    )

Step 5b — SUSPICIOUS branch

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
elif verdict == 'SUSPICIOUS':
    # Create P3 ticket and assign to analyst for manual review
    ticket = servicenow_create_incident(
        priority=3,
        short_desc=f"Phishing (Suspicious): Manual review required — {headers.from_addr}",
        description=case.build_report()
    )
    teams_notify(
        channel='#soc-alerts',
        message=f"[P3] Suspicious email flagged for review. Ticket: {ticket.number}"
    )

Playbook 2: Compromised Account Response

Decision Tree

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
Alert: Anomalous Login / ATO Detected
        Gather user context
        (role, department, MFA status)
        Verify with user
        (automated Teams message)
        ┌───────┴────────┐
    Confirms         No response
    compromise?      in 15 min?
        │                │
        ▼                ▼
   Treat as         Treat as
   confirmed        confirmed
  Disable account (AD/Entra ID)
  Revoke all sessions
  Check for lateral movement
  (SIEM: auth logs ±2hr window)
  Scope the blast radius
  (data accessed, emails sent)
  Escalate or close

Step-by-Step Logic

Step 1 — Trigger

The playbook fires when:

  • SIEM fires on impossible travel (login from two countries within 2 hours)
  • SIEM fires on password spray success (multiple failed logins followed by a success)
  • Entra ID Identity Protection raises a high-risk sign-in

Step 2 — Gather Context

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
user = entra_get_user(alert.upn)
context = {
    'display_name':  user.display_name,
    'department':    user.department,
    'manager':       user.manager,
    'mfa_enabled':   user.mfa_methods is not None,
    'last_password_change': user.last_password_change,
    'privileged':    'Admin' in user.roles or 'Service' in user.roles,
}

# Privileged accounts skip the verification step — auto-contain immediately
if context['privileged']:
    skip_to_containment = True

Step 3 — Verify with the user (non-privileged)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if not skip_to_containment:
    message = teams_send_dm(
        user=alert.upn,
        message=(
            f"Hi {context['display_name']}, our security team detected an unusual "
            f"sign-in to your account from {alert.location} at {alert.login_time}.\n\n"
            f"Was this you?\n\n"
            f"✅ Reply YES if this was you\n"
            f"❌ Reply NO if this was NOT you"
        )
    )
    # Wait up to 15 minutes for a response
    response = wait_for_reply(message.id, timeout_minutes=15)

    if response is None or 'no' in response.lower():
        confirmed_compromise = True
    else:
        # User confirmed it was them — close with note
        case.close(reason='Confirmed legitimate login by user')
        return

Step 4 — Containment

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
if confirmed_compromise:
    # 1. Disable the account
    entra_disable_account(alert.upn)

    # 2. Revoke all active sessions
    entra_revoke_sessions(alert.upn)

    # 3. Force MFA re-registration on next login
    entra_require_mfa_reregister(alert.upn)

    # 4. Notify the user's manager
    teams_notify(
        user=context['manager'],
        message=(
            f"Security Alert: {context['display_name']}'s account has been disabled "
            f"due to a suspected compromise. Please contact the security team."
        )
    )

Step 5 — Investigate lateral movement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Pull authentication logs for the 2 hours before and after the suspicious login
auth_logs = siem_search(
    query=f'user="{alert.upn}" AND eventtype="authentication"',
    timerange=f'{alert.login_time - 2h} TO {alert.login_time + 2h}'
)

# Look for resources accessed under this session
accessed_resources = entra_get_sign_in_logs(
    upn=alert.upn,
    session_id=alert.session_id
)

lateral_movement_indicators = [
    log for log in auth_logs
    if log.source_ip == alert.source_ip
    and log.resource != 'Microsoft 365'
]

Step 6 — Create ticket with full investigation summary

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
ticket = servicenow_create_incident(
    priority=2 if not context['privileged'] else 1,
    short_desc=f"Account Compromise: {alert.upn}",
    description=f"""
Account: {alert.upn}
Department: {context['department']}
Suspicious login: {alert.location} at {alert.login_time}

Containment actions taken:
- Account disabled: YES
- Sessions revoked: YES
- MFA re-registration required: YES

Lateral movement: {len(lateral_movement_indicators)} suspicious auth events
Resources accessed: {[r.resource_name for r in accessed_resources]}

Next steps:
1. Interview user and manager
2. Review all emails sent from account in the compromise window
3. Check for inbox rules created by the attacker
4. Determine initial access vector
"""
)

Platform Adaptation Notes

SOAR Platform Scripting Notes
Palo Alto XSOAR Python Use demisto.executeCommand() for integrations
Splunk SOAR Python Use phantom.act() for actions
Microsoft Sentinel Logic Apps / KQL Use HTTP connectors for non-native integrations
IBM Resilient Python Use resilient-circuits SDK

Contact me at contact@malsayegh.ae to discuss playbook design for your specific SOAR platform.

comments powered by Disqus
All rights Reserved for malsayegh.ae
Built with Hugo
Theme Stack designed by Jimmy