Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Investigates phishing email incidents from initial user report through header analysis, URL/attachment detonation, impacted user identification, and containment actions using SOC tools like Splunk, Microsoft Defender, and sandbox analysis platforms. Use when a reported phishing email requires full incident investigation to determine scope and impact.
.claude/skills/investigating-phishing-email-incident/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-06 | ✗→✓ | ▲ Improved | — | — |
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-10 | ✗→✓ | ▲ Improved | — | — |
| case-19 | ✗→✓ | ▲ Improved | — | — |
| case-22 | ✗→✓ | ▲ Improved | — | — |
Use this skill when:
Do not use for spam or marketing emails without malicious intent — route those to email administration for filter tuning.
Obtain the full email headers (.eml file) from the reported message:
pythonimport email from email import policy with open("phishing_sample.eml", "rb") as f: msg = email.message_from_binary_file(f, policy=policy.default) # Extract key headers print(f"From: {msg['From']}") print(f"Return-Path: {msg['Return-Path']}") print(f"Reply-To: {msg['Reply-To']}") print(f"Subject: {msg['Subject']}") print(f"Message-ID: {msg['Message-ID']}") print(f"X-Originating-IP: {msg['X-Originating-IP']}") # Parse Received headers (bottom-up for true origin) for header in reversed(msg.get_all('Received', [])): print(f"Received: {header[:120]}") # Check authentication results print(f"Authentication-Results: {msg['Authentication-Results']}") print(f"DKIM-Signature: {msg.get('DKIM-Signature', 'NONE')[:80]}")
Key checks:
Return-Path domain match sending IP? Look for spf=pass or spf=faildkim=pass confirms the email was not modified in transitFrom domain align with SPF/DKIM domains? dmarc=fail indicates spoofingURL Analysis:
pythonimport requests # Submit URL to URLScan.io url_to_scan = "https://evil-login.example.com/office365" response = requests.post( "https://urlscan.io/api/v1/scan/", headers={"API-Key": "YOUR_KEY", "Content-Type": "application/json"}, json={"url": url_to_scan, "visibility": "unlisted"} ) scan_id = response.json()["uuid"] print(f"Scan URL: https://urlscan.io/result/{scan_id}/") # Check VirusTotal for URL reputation import vt client = vt.Client("YOUR_VT_API_KEY") url_id = vt.url_id(url_to_scan) url_obj = client.get_object(f"/urls/{url_id}") print(f"VT Score: {url_obj.last_analysis_stats}") client.close()
Attachment Analysis:
pythonimport hashlib # Calculate file hashes with open("attachment.docx", "rb") as f: content = f.read() md5 = hashlib.md5(content).hexdigest() sha256 = hashlib.sha256(content).hexdigest() print(f"MD5: {md5}") print(f"SHA256: {sha256}") # Submit to MalwareBazaar for lookup response = requests.post( "https://mb-api.abuse.ch/api/v1/", data={"query": "get_info", "hash": sha256} ) print(response.json()["query_status"])
Submit to sandbox (Any.Run or Joe Sandbox) for dynamic analysis of macros, PowerShell execution, and C2 callbacks.
Search for all recipients of the same phishing email in Splunk:
splindex=email sourcetype="o365:messageTrace" (SenderAddress="attacker@evil-domain.com" OR Subject="Urgent: Password Reset Required" OR MessageId="<phishing-message-id@evil.com>") earliest=-7d | stats count by RecipientAddress, DeliveryStatus, MessageTraceId | sort - count
Alternatively, use Microsoft Graph API:
pythonimport requests headers = {"Authorization": f"Bearer {access_token}"} params = { "$filter": f"subject eq 'Urgent: Password Reset Required' and " f"receivedDateTime ge 2024-03-14T00:00:00Z", "$select": "sender,toRecipients,subject,receivedDateTime", "$top": 100 } response = requests.get( "https://graph.microsoft.com/v1.0/users/admin@company.com/messages", headers=headers, params=params ) messages = response.json()["value"] print(f"Found {len(messages)} matching messages")
Check proxy/web logs for users who visited the phishing URL:
splindex=proxy dest="evil-login.example.com" earliest=-7d | stats count, values(action) AS actions, latest(_time) AS last_access by src_ip, user | lookup asset_lookup_by_cidr ip AS src_ip OUTPUT owner, category | sort - count | table user, src_ip, owner, actions, count, last_access
Check if credentials were submitted (POST requests to phishing domain):
splindex=proxy dest="evil-login.example.com" http_method=POST earliest=-7d | stats count by src_ip, user, url, status
Purge emails from all mailboxes:
powershell# Microsoft 365 Compliance Search and Purge New-ComplianceSearch -Name "Phishing_Purge_2024_0315" ` -ExchangeLocation All ` -ContentMatchQuery '(From:attacker@evil-domain.com) AND (Subject:"Urgent: Password Reset Required")' Start-ComplianceSearch -Identity "Phishing_Purge_2024_0315" # After search completes, execute purge New-ComplianceSearchAction -SearchName "Phishing_Purge_2024_0315" -Purge -PurgeType SoftDelete
Block indicators:
Reset compromised credentials:
powershell# Force password reset for impacted users $impactedUsers = @("user1@company.com", "user2@company.com") foreach ($user in $impactedUsers) { Set-MsolUserPassword -UserPrincipalName $user -ForceChangePassword $true Revoke-AzureADUserAllRefreshToken -ObjectId (Get-AzureADUser -ObjectId $user).ObjectId }
Create incident report with full timeline, IOCs, impacted users, and remediation actions taken.
spl| makeresults | eval incident_id="PHI-2024-0315", reported_time="2024-03-15 09:12:00", sender="attacker@evil-domain[.]com", subject="Urgent: Password Reset Required", url="hxxps://evil-login[.]example[.]com/office365", recipients_count=47, clicked_count=5, credentials_submitted=2, emails_purged=47, passwords_reset=2, domains_blocked=1, disposition="True Positive - Credential Phishing Campaign" | table incident_id, reported_time, sender, subject, url, recipients_count, clicked_count, credentials_submitted, emails_purged, passwords_reset, disposition
| Term | Definition | |------|-----------| | SPF (Sender Policy Framework) | DNS TXT record specifying which mail servers are authorized to send on behalf of a domain | | DKIM | DomainKeys Identified Mail — cryptographic signature proving email content was not altered in transit | | DMARC | Domain-based Message Authentication, Reporting and Conformance — policy combining SPF and DKIM alignment | | Credential Harvesting | Phishing technique using fake login pages to capture username/password combinations | | Business Email Compromise (BEC) | Social engineering attack using compromised or spoofed executive email for financial fraud | | Message Trace | O365/Exchange log showing email routing, delivery status, and filtering actions for forensic analysis |
PHISHING INCIDENT REPORT — PHI-2024-0315
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Reported: 2024-03-15 09:12 UTC by jsmith (Finance)
Sender: attacker@evil-domain[.]com (SPF: FAIL, DKIM: NONE, DMARC: FAIL)
Subject: Urgent: Password Reset Required
Payload: Credential harvesting URL
IOCs:
URL: hxxps://evil-login[.]example[.]com/office365
Domain: evil-login[.]example[.]com (registered 2024-03-14, Namecheap)
IP: 185.234.xx.xx (VT: 12/90 malicious)
Scope:
Recipients: 47 users across Finance and HR departments
Clicked: 5 users visited phishing URL
Submitted: 2 users entered credentials (confirmed via POST in proxy logs)
Containment:
[DONE] 47 emails purged via Compliance Search
[DONE] Domain blocked on proxy and DNS sinkhole
[DONE] 2 user passwords reset, sessions revoked
[DONE] MFA enforced for both compromised accounts
[DONE] Inbox rules audited — no forwarding rules found
Status: RESOLVED — No evidence of lateral movement post-compromise| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-14 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
Other measured skills in the registry, with their headline benchmark lift.