Files
mukul975 27c6414ca5 Add folder anatomy (scripts/agent.py + references/api-reference.md) for 648 cybersecurity skills
Complete skill folder anatomy across all cybersecurity skills:
- scripts/agent.py: 80-150 line Python agents using real libraries (impacket,
  boto3, azure-mgmt-*, kubernetes, pefile, yara, scapy, shodan, stix2, etc.)
- references/api-reference.md: real API documentation with method signatures
- LICENSE: MIT license for all skill folders
2026-03-10 21:02:12 +01:00

265 lines
8.9 KiB
Python

#!/usr/bin/env python3
"""Email Security Audit Agent - Validates SPF, DKIM, and DMARC DNS records for domains."""
import json
import re
import logging
import argparse
from datetime import datetime
import dns.resolver
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
logger = logging.getLogger(__name__)
DKIM_SELECTORS = [
"default", "google", "selector1", "selector2", "k1", "k2",
"mail", "dkim", "s1", "s2", "mandrill", "everlytickey1",
"smtpapi", "pic", "protonmail", "protonmail2", "protonmail3",
]
def query_txt_records(domain, prefix=""):
"""Query TXT DNS records for a domain."""
fqdn = f"{prefix}.{domain}" if prefix else domain
try:
answers = dns.resolver.resolve(fqdn, "TXT")
records = []
for rdata in answers:
txt = b"".join(rdata.strings).decode("utf-8", errors="ignore")
records.append(txt)
return records
except (dns.resolver.NXDOMAIN, dns.resolver.NoAnswer, dns.resolver.NoNameservers, dns.exception.Timeout):
return []
def check_spf(domain):
"""Check and validate SPF record."""
records = query_txt_records(domain)
spf_records = [r for r in records if r.startswith("v=spf1")]
if not spf_records:
return {"status": "missing", "severity": "critical", "issues": ["No SPF record found"], "record": None}
if len(spf_records) > 1:
issues = ["Multiple SPF records found (RFC violation, causes permerror)"]
else:
issues = []
spf = spf_records[0]
mechanisms = spf.split()
include_count = sum(1 for m in mechanisms if m.startswith("include:"))
has_all = any(m in ("~all", "-all", "+all", "?all") for m in mechanisms)
if "+all" in mechanisms:
issues.append("SPF uses +all (allows any sender - completely open)")
severity = "critical"
elif "?all" in mechanisms:
issues.append("SPF uses ?all (neutral - no protection)")
severity = "high"
elif "~all" in mechanisms:
issues.append("SPF uses ~all (softfail - mail accepted but marked)")
severity = "medium"
elif "-all" in mechanisms:
severity = "low"
elif not has_all:
issues.append("SPF record missing -all qualifier")
severity = "high"
else:
severity = "low"
if include_count > 10:
issues.append(f"SPF has {include_count} includes (>10 DNS lookups causes permerror)")
severity = "high"
lookup_mechanisms = sum(1 for m in mechanisms if any(m.startswith(p) for p in ("include:", "a:", "mx:", "ptr:", "exists:", "redirect=")))
if lookup_mechanisms > 10:
issues.append(f"SPF exceeds 10 DNS lookup limit ({lookup_mechanisms} lookups)")
return {
"status": "found",
"record": spf,
"mechanism_count": len(mechanisms),
"include_count": include_count,
"dns_lookups": lookup_mechanisms,
"qualifier": next((m for m in mechanisms if m.endswith("all")), "none"),
"severity": severity,
"issues": issues,
}
def check_dkim(domain, selectors=None):
"""Check DKIM records for common selectors."""
if selectors is None:
selectors = DKIM_SELECTORS
found_selectors = []
for selector in selectors:
records = query_txt_records(domain, prefix=f"{selector}._domainkey")
dkim_records = [r for r in records if "v=DKIM1" in r or "k=rsa" in r or "p=" in r]
if dkim_records:
record = dkim_records[0]
key_match = re.search(r"p=([A-Za-z0-9+/=]+)", record)
key_length = len(key_match.group(1)) * 6 // 8 if key_match else 0
issues = []
if key_length and key_length < 128:
issues.append(f"DKIM key too short ({key_length} bytes, minimum 1024 bits recommended)")
if "p=" in record and not key_match:
issues.append("DKIM public key appears empty (revoked)")
found_selectors.append({
"selector": selector,
"record": record[:200],
"key_size_bytes": key_length,
"issues": issues,
})
if not found_selectors:
return {
"status": "not_found",
"severity": "high",
"issues": ["No DKIM records found for any common selector"],
"selectors_checked": len(selectors),
"selectors_found": [],
}
return {
"status": "found",
"severity": "low",
"selectors_checked": len(selectors),
"selectors_found": found_selectors,
"issues": [i for s in found_selectors for i in s["issues"]],
}
def check_dmarc(domain):
"""Check and validate DMARC record."""
records = query_txt_records(domain, prefix="_dmarc")
dmarc_records = [r for r in records if r.startswith("v=DMARC1")]
if not dmarc_records:
return {"status": "missing", "severity": "critical", "issues": ["No DMARC record found"], "record": None}
dmarc = dmarc_records[0]
tags = {}
for part in dmarc.split(";"):
part = part.strip()
if "=" in part:
key, val = part.split("=", 1)
tags[key.strip()] = val.strip()
issues = []
policy = tags.get("p", "none")
subdomain_policy = tags.get("sp", policy)
pct = int(tags.get("pct", "100"))
rua = tags.get("rua", "")
ruf = tags.get("ruf", "")
adkim = tags.get("adkim", "r")
aspf = tags.get("aspf", "r")
if policy == "none":
issues.append("DMARC policy is 'none' - no enforcement (monitoring only)")
severity = "high"
elif policy == "quarantine":
severity = "medium" if pct < 100 else "low"
if pct < 100:
issues.append(f"DMARC only applied to {pct}% of messages")
elif policy == "reject":
severity = "low"
if pct < 100:
issues.append(f"DMARC reject only applied to {pct}% of messages")
severity = "medium"
else:
severity = "high"
issues.append(f"Unknown DMARC policy: {policy}")
if not rua:
issues.append("No aggregate report URI (rua) configured")
if not ruf:
issues.append("No forensic report URI (ruf) configured")
if adkim == "r":
issues.append("DKIM alignment is relaxed (adkim=r)")
if aspf == "r":
issues.append("SPF alignment is relaxed (aspf=r)")
return {
"status": "found",
"record": dmarc,
"policy": policy,
"subdomain_policy": subdomain_policy,
"percentage": pct,
"aggregate_report": rua,
"forensic_report": ruf,
"dkim_alignment": adkim,
"spf_alignment": aspf,
"severity": severity,
"issues": issues,
}
def compute_risk_score(spf, dkim, dmarc):
"""Compute overall email security risk score."""
severity_scores = {"critical": 40, "high": 25, "medium": 10, "low": 0}
score = 0
score += severity_scores.get(spf["severity"], 0)
score += severity_scores.get(dkim["severity"], 0)
score += severity_scores.get(dmarc["severity"], 0)
if spf["status"] == "missing":
score += 20
if dmarc.get("policy") == "none":
score += 15
if score >= 60:
risk = "CRITICAL"
elif score >= 35:
risk = "HIGH"
elif score >= 15:
risk = "MEDIUM"
else:
risk = "LOW"
return {"score": score, "risk_level": risk}
def generate_report(domain, spf, dkim, dmarc, risk):
"""Generate email security audit report."""
all_issues = spf.get("issues", []) + dkim.get("issues", []) + dmarc.get("issues", [])
report = {
"timestamp": datetime.utcnow().isoformat(),
"domain": domain,
"risk_assessment": risk,
"spf": spf,
"dkim": dkim,
"dmarc": dmarc,
"total_issues": len(all_issues),
"all_issues": all_issues,
}
print(f"EMAIL SECURITY [{domain}]: Risk={risk['risk_level']} Score={risk['score']} Issues={len(all_issues)}")
return report
def main():
parser = argparse.ArgumentParser(description="Email Security Audit Agent (SPF/DKIM/DMARC)")
parser.add_argument("--domain", required=True, help="Domain to audit")
parser.add_argument("--dkim-selectors", nargs="*", help="Custom DKIM selectors to check")
parser.add_argument("--output", default="email_security_report.json")
args = parser.parse_args()
spf = check_spf(args.domain)
logger.info("SPF: %s (severity: %s)", spf["status"], spf["severity"])
dkim = check_dkim(args.domain, args.dkim_selectors)
logger.info("DKIM: %s (%d selectors found)", dkim["status"], len(dkim.get("selectors_found", [])))
dmarc = check_dmarc(args.domain)
logger.info("DMARC: %s (severity: %s)", dmarc["status"], dmarc["severity"])
risk = compute_risk_score(spf, dkim, dmarc)
report = generate_report(args.domain, spf, dkim, dmarc, risk)
with open(args.output, "w") as f:
json.dump(report, f, indent=2)
logger.info("Report saved to %s", args.output)
if __name__ == "__main__":
main()