mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-13 22:54:53 +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
205 lines
8.5 KiB
Python
205 lines
8.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Email Account Compromise Detection agent - analyzes inbox rules, sign-in logs, and OAuth grants to detect O365/Google Workspace account compromise"""
|
|
|
|
import argparse
|
|
import json
|
|
import math
|
|
from collections import Counter, defaultdict
|
|
from datetime import datetime
|
|
from pathlib import Path
|
|
|
|
SUSPICIOUS_UA_PATTERNS = [
|
|
"python-requests", "python-urllib", "curl", "wget", "powershell",
|
|
"go-http-client", "httpie", "postman", "insomnia",
|
|
]
|
|
|
|
FINANCIAL_KEYWORDS = [
|
|
"invoice", "payment", "wire", "transfer", "bank", "account",
|
|
"payroll", "salary", "remittance", "ach", "swift",
|
|
]
|
|
|
|
|
|
def load_data(path):
|
|
return json.loads(Path(path).read_text(encoding="utf-8"))
|
|
|
|
|
|
def haversine_km(lat1, lon1, lat2, lon2):
|
|
R = 6371.0
|
|
rlat1, rlat2 = math.radians(lat1), math.radians(lat2)
|
|
dlat = math.radians(lat2 - lat1)
|
|
dlon = math.radians(lon2 - lon1)
|
|
a = math.sin(dlat / 2) ** 2 + math.cos(rlat1) * math.cos(rlat2) * math.sin(dlon / 2) ** 2
|
|
return R * 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
|
|
|
|
|
|
def analyze_inbox_rules(rules):
|
|
"""Detect malicious inbox rules: external forwarding, deletion rules, keyword-based filters."""
|
|
findings = []
|
|
for rule in rules:
|
|
user = rule.get("user", rule.get("mailbox", ""))
|
|
rule_name = rule.get("displayName", rule.get("name", ""))
|
|
actions = rule.get("actions", {})
|
|
forward_to = actions.get("forwardTo", []) or actions.get("forward_to", [])
|
|
redirect_to = actions.get("redirectTo", []) or actions.get("redirect_to", [])
|
|
delete_msg = actions.get("delete", False) or actions.get("moveToDeletedItems", False)
|
|
move_to = actions.get("moveToFolder", "") or ""
|
|
conditions = rule.get("conditions", {})
|
|
subject_contains = conditions.get("subjectContains", []) or []
|
|
body_contains = conditions.get("bodyContains", []) or []
|
|
for dest in forward_to + redirect_to:
|
|
addr = dest.get("emailAddress", {}).get("address", dest) if isinstance(dest, dict) else str(dest)
|
|
domain = addr.split("@")[-1] if "@" in str(addr) else ""
|
|
user_domain = user.split("@")[-1] if "@" in user else ""
|
|
if domain and domain != user_domain:
|
|
findings.append({
|
|
"type": "external_forwarding_rule",
|
|
"severity": "critical",
|
|
"resource": user,
|
|
"detail": f"Rule '{rule_name}' forwards to external address: {addr}",
|
|
})
|
|
if delete_msg:
|
|
findings.append({
|
|
"type": "deletion_rule",
|
|
"severity": "high",
|
|
"resource": user,
|
|
"detail": f"Rule '{rule_name}' auto-deletes messages",
|
|
})
|
|
keyword_hits = [kw for kw in FINANCIAL_KEYWORDS
|
|
if any(kw in s.lower() for s in subject_contains + body_contains)]
|
|
if keyword_hits:
|
|
findings.append({
|
|
"type": "financial_keyword_filter",
|
|
"severity": "high",
|
|
"resource": user,
|
|
"detail": f"Rule '{rule_name}' targets financial keywords: {', '.join(keyword_hits)}",
|
|
})
|
|
return findings
|
|
|
|
|
|
def analyze_sign_ins(sign_ins):
|
|
"""Detect impossible travel, suspicious user agents, and risky sign-in patterns."""
|
|
findings = []
|
|
user_logins = defaultdict(list)
|
|
for si in sign_ins:
|
|
user = si.get("userPrincipalName", si.get("user", ""))
|
|
ua = si.get("userAgent", si.get("user_agent", ""))
|
|
ip = si.get("ipAddress", si.get("ip", ""))
|
|
ts = si.get("createdDateTime", si.get("timestamp", ""))
|
|
lat = si.get("location", {}).get("geoCoordinates", {}).get("latitude", 0)
|
|
lon = si.get("location", {}).get("geoCoordinates", {}).get("longitude", 0)
|
|
country = si.get("location", {}).get("countryOrRegion", "")
|
|
risk = si.get("riskLevelAggregated", si.get("risk_level", "none"))
|
|
for pattern in SUSPICIOUS_UA_PATTERNS:
|
|
if pattern.lower() in (ua or "").lower():
|
|
findings.append({
|
|
"type": "suspicious_user_agent",
|
|
"severity": "high",
|
|
"resource": user,
|
|
"detail": f"Sign-in from suspicious UA '{ua[:60]}' at IP {ip}",
|
|
})
|
|
break
|
|
if risk in ("high", "medium"):
|
|
findings.append({
|
|
"type": "risky_sign_in",
|
|
"severity": "high" if risk == "high" else "medium",
|
|
"resource": user,
|
|
"detail": f"Azure AD risk level '{risk}' from {country or ip}",
|
|
})
|
|
if lat and lon and ts:
|
|
user_logins[user].append({"ts": ts, "lat": lat, "lon": lon, "ip": ip})
|
|
for user, logins in user_logins.items():
|
|
try:
|
|
logins.sort(key=lambda x: x["ts"])
|
|
except TypeError:
|
|
continue
|
|
for i in range(1, len(logins)):
|
|
try:
|
|
t1 = datetime.fromisoformat(logins[i - 1]["ts"].replace("Z", "+00:00"))
|
|
t2 = datetime.fromisoformat(logins[i]["ts"].replace("Z", "+00:00"))
|
|
hours = abs((t2 - t1).total_seconds()) / 3600.0
|
|
if hours < 0.01:
|
|
continue
|
|
dist = haversine_km(logins[i - 1]["lat"], logins[i - 1]["lon"], logins[i]["lat"], logins[i]["lon"])
|
|
speed = dist / hours
|
|
if speed > 900:
|
|
findings.append({
|
|
"type": "impossible_travel",
|
|
"severity": "critical",
|
|
"resource": user,
|
|
"detail": f"Impossible travel: {dist:.0f} km in {hours:.1f}h ({speed:.0f} km/h) between IPs {logins[i-1]['ip']} and {logins[i]['ip']}",
|
|
})
|
|
except (ValueError, TypeError):
|
|
continue
|
|
return findings
|
|
|
|
|
|
def analyze_oauth_grants(grants):
|
|
"""Detect suspicious OAuth application consent grants."""
|
|
findings = []
|
|
for grant in grants:
|
|
user = grant.get("user", grant.get("principalDisplayName", ""))
|
|
app = grant.get("appDisplayName", grant.get("app_name", ""))
|
|
scopes = grant.get("scope", grant.get("scopes", ""))
|
|
consent_type = grant.get("consentType", "")
|
|
risky_scopes = ["Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite", "Files.ReadWrite.All"]
|
|
granted_risky = [s for s in risky_scopes if s.lower() in (scopes or "").lower()]
|
|
if granted_risky:
|
|
findings.append({
|
|
"type": "risky_oauth_grant",
|
|
"severity": "high",
|
|
"resource": user,
|
|
"detail": f"App '{app}' granted risky scopes: {', '.join(granted_risky)}",
|
|
})
|
|
if consent_type == "AllPrincipals":
|
|
findings.append({
|
|
"type": "admin_consent_grant",
|
|
"severity": "critical",
|
|
"resource": user,
|
|
"detail": f"App '{app}' has admin consent (AllPrincipals) with scopes: {scopes[:80]}",
|
|
})
|
|
return findings
|
|
|
|
|
|
def analyze(data):
|
|
findings = []
|
|
if isinstance(data, list):
|
|
findings.extend(analyze_inbox_rules(data))
|
|
return findings
|
|
findings.extend(analyze_inbox_rules(data.get("inbox_rules", data.get("rules", []))))
|
|
findings.extend(analyze_sign_ins(data.get("sign_ins", data.get("logins", []))))
|
|
findings.extend(analyze_oauth_grants(data.get("oauth_grants", data.get("app_consents", []))))
|
|
return findings
|
|
|
|
|
|
def generate_report(input_path):
|
|
data = load_data(input_path)
|
|
findings = analyze(data)
|
|
sev = Counter(f["severity"] for f in findings)
|
|
compromised = set(f["resource"] for f in findings if f["severity"] in ("critical", "high"))
|
|
return {
|
|
"report": "email_account_compromise_detection",
|
|
"generated_at": datetime.utcnow().isoformat() + "Z",
|
|
"total_findings": len(findings),
|
|
"severity_summary": dict(sev),
|
|
"potentially_compromised_accounts": sorted(compromised),
|
|
"findings": findings,
|
|
}
|
|
|
|
|
|
def main():
|
|
ap = argparse.ArgumentParser(description="Email Account Compromise Detection Agent")
|
|
ap.add_argument("--input", required=True, help="Input JSON with inbox rules/sign-in/OAuth data")
|
|
ap.add_argument("--output", help="Output JSON report path")
|
|
args = ap.parse_args()
|
|
report = generate_report(args.input)
|
|
out = json.dumps(report, indent=2)
|
|
if args.output:
|
|
Path(args.output).write_text(out, encoding="utf-8")
|
|
print(f"Report written to {args.output}")
|
|
else:
|
|
print(out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|