mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-16 16:03:17 +03:00
c47eed6a64
- 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
158 lines
5.5 KiB
Python
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()
|