Overview
A SOAR playbook is only as good as its logic. This page documents two production playbooks:
- Phishing Response — from reported email to full remediation
- 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
"""
)
|
| 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.