Files
Anthropic-Cybersecurity-Skills/skills/detecting-business-email-compromise/scripts/agent.py
T
mukul975 c47eed6a64 Production hardening: security fixes, code quality, 724 skills complete
- Fix 25 shell=True subprocess calls with list-based commands
- Fix 49 verify=False in defensive skills (env-var override)
- Add timeout to 231 HTTP/subprocess/socket calls
- Fix 6 SQL injection patterns with whitelist validation
- Replace 8 __import__() with standard imports
- Remove 701 unused imports across 442 files
- Add authorized-testing disclaimers to all offensive skills
- Complete 11 incomplete skill directories
- Expand 10 stub SKILL.md files with full content
- Fix 2 YAML parse errors in frontmatter
- Fix 5 pre-existing syntax errors
- Convert 22 hardcoded paths/ports to environment variables
- Back up 21 redundant skill pairs to .bak
- Fix 2 global declaration errors
- 724/724 skills with full folder anatomy (SKILL.md + agent.py + api-reference.md + LICENSE)
- 0 compile errors across all 724 agent.py files
2026-03-19 13:26:49 +01:00

158 lines
5.5 KiB
Python

#!/usr/bin/env python3
"""BEC detection agent - analyzes email headers and content for Business Email Compromise indicators.
Parses email headers for spoofing signals, checks DMARC/SPF/DKIM alignment,
detects urgency language patterns, and flags financial request anomalies.
"""
import argparse
import email
import json
import re
from email import policy
from pathlib import Path
BEC_URGENCY_PATTERNS = [
r"\b(urgent|immediately|asap|right away|time.?sensitive)\b",
r"\b(confidential|do not share|keep this between us|don't tell)\b",
r"\b(wire transfer|bank transfer|payment|invoice|routing number)\b",
r"\b(gift card|bitcoin|crypto|western union|moneygram)\b",
r"\b(ceo|cfo|president|director) (asked|requested|needs|wants)\b",
r"\b(change.*(bank|account|payment)|new.*(bank|account|routing))\b",
r"\b(act now|deadline today|end of day|before close)\b",
]
EXECUTIVE_TITLES = ["ceo", "cfo", "coo", "cto", "president", "chairman",
"managing director", "vice president", "vp", "director"]
def parse_email_file(filepath):
with open(filepath, "r", encoding="utf-8", errors="replace") as f:
return email.message_from_file(f, policy=policy.default)
def check_spf_dkim_dmarc(msg):
results = {"spf": "none", "dkim": "none", "dmarc": "none"}
auth_results = msg.get("Authentication-Results", "")
if "spf=pass" in auth_results.lower():
results["spf"] = "pass"
elif "spf=fail" in auth_results.lower():
results["spf"] = "fail"
if "dkim=pass" in auth_results.lower():
results["dkim"] = "pass"
elif "dkim=fail" in auth_results.lower():
results["dkim"] = "fail"
if "dmarc=pass" in auth_results.lower():
results["dmarc"] = "pass"
elif "dmarc=fail" in auth_results.lower():
results["dmarc"] = "fail"
return results
def check_display_name_spoofing(msg, vip_names):
from_header = msg.get("From", "")
match = re.match(r'"?([^"<]+)"?\s*<([^>]+)>', from_header)
if not match:
return None
display_name = match.group(1).strip().lower()
email_addr = match.group(2).strip().lower()
for vip in vip_names:
if vip.lower() in display_name:
domain = email_addr.split("@")[-1] if "@" in email_addr else ""
return {"display_name": display_name, "email": email_addr,
"matched_vip": vip, "domain": domain,
"indicator": "Display name matches VIP but email may be external"}
return None
def check_reply_to_mismatch(msg):
from_addr = msg.get("From", "")
reply_to = msg.get("Reply-To", "")
if not reply_to:
return None
from_match = re.search(r'<([^>]+)>', from_addr) or re.search(r'(\S+@\S+)', from_addr)
reply_match = re.search(r'<([^>]+)>', reply_to) or re.search(r'(\S+@\S+)', reply_to)
if from_match and reply_match:
from_email = from_match.group(1).lower()
reply_email = reply_match.group(1).lower()
from_domain = from_email.split("@")[-1]
reply_domain = reply_email.split("@")[-1]
if from_domain != reply_domain:
return {"from": from_email, "reply_to": reply_email,
"indicator": "Reply-To domain differs from From domain"}
return None
def detect_urgency_language(body):
matches = []
for pattern in BEC_URGENCY_PATTERNS:
found = re.findall(pattern, body, re.IGNORECASE)
if found:
matches.extend(found)
return matches
def calculate_bec_score(auth, spoofing, reply_mismatch, urgency_matches):
score = 0
if auth.get("spf") == "fail":
score += 25
if auth.get("dkim") == "fail":
score += 20
if auth.get("dmarc") == "fail":
score += 30
if spoofing:
score += 35
if reply_mismatch:
score += 25
score += min(len(urgency_matches) * 10, 40)
return min(score, 100)
def analyze_email(filepath, vip_names):
msg = parse_email_file(filepath)
body = msg.get_body(preferencelist=("plain", "html"))
body_text = body.get_content() if body else ""
auth = check_spf_dkim_dmarc(msg)
spoofing = check_display_name_spoofing(msg, vip_names)
reply_mismatch = check_reply_to_mismatch(msg)
urgency = detect_urgency_language(body_text)
score = calculate_bec_score(auth, spoofing, reply_mismatch, urgency)
risk = "CRITICAL" if score >= 70 else "HIGH" if score >= 50 else "MEDIUM" if score >= 30 else "LOW"
return {
"file": str(filepath),
"from": msg.get("From", ""),
"to": msg.get("To", ""),
"subject": msg.get("Subject", ""),
"date": msg.get("Date", ""),
"authentication": auth,
"display_name_spoofing": spoofing,
"reply_to_mismatch": reply_mismatch,
"urgency_indicators": urgency,
"bec_score": score,
"risk_level": risk,
}
def main():
parser = argparse.ArgumentParser(description="BEC Email Analyzer")
parser.add_argument("--email-file", required=True, help="Path to .eml file")
parser.add_argument("--vip-names", nargs="+", default=[], help="VIP display names to check")
parser.add_argument("--scan-dir", help="Scan all .eml files in directory")
args = parser.parse_args()
results = []
if args.scan_dir:
for eml in Path(args.scan_dir).glob("*.eml"):
results.append(analyze_email(str(eml), args.vip_names))
else:
results.append(analyze_email(args.email_file, args.vip_names))
print(json.dumps(results, indent=2))
if __name__ == "__main__":
main()