mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-11 13:44:56 +03:00
336 lines
11 KiB
Python
336 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Proofpoint Email Security Gateway Configuration Validator
|
|
|
|
Validates MX records, SPF/DKIM/DMARC alignment, and Proofpoint
|
|
mail flow configuration for a given domain. Uses DNS lookups to
|
|
verify that email is routing through Proofpoint correctly.
|
|
|
|
Usage:
|
|
python process.py check-mx --domain example.com
|
|
python process.py check-auth --domain example.com
|
|
python process.py validate-headers --eml-file message.eml
|
|
"""
|
|
|
|
import argparse
|
|
import json
|
|
import re
|
|
import sys
|
|
from dataclasses import dataclass, field, asdict
|
|
from typing import Optional
|
|
|
|
try:
|
|
import dns.resolver
|
|
HAS_DNS = True
|
|
except ImportError:
|
|
HAS_DNS = False
|
|
|
|
|
|
@dataclass
|
|
class MXCheckResult:
|
|
"""MX record validation result."""
|
|
domain: str = ""
|
|
mx_records: list = field(default_factory=list)
|
|
routes_through_proofpoint: bool = False
|
|
proofpoint_mx: str = ""
|
|
issues: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class AuthCheckResult:
|
|
"""Email authentication check result."""
|
|
domain: str = ""
|
|
spf_record: str = ""
|
|
spf_includes_proofpoint: bool = False
|
|
dmarc_record: str = ""
|
|
dmarc_policy: str = ""
|
|
dkim_selector: str = ""
|
|
issues: list = field(default_factory=list)
|
|
|
|
|
|
@dataclass
|
|
class HeaderAnalysis:
|
|
"""Email header forensic analysis."""
|
|
from_address: str = ""
|
|
return_path: str = ""
|
|
received_chain: list = field(default_factory=list)
|
|
spf_result: str = ""
|
|
dkim_result: str = ""
|
|
dmarc_result: str = ""
|
|
proofpoint_headers: list = field(default_factory=list)
|
|
x_proofpoint_spam_details: str = ""
|
|
x_proofpoint_virus_version: str = ""
|
|
passed_through_proofpoint: bool = False
|
|
issues: list = field(default_factory=list)
|
|
|
|
|
|
PROOFPOINT_MX_PATTERNS = [
|
|
r'\.pphosted\.com$',
|
|
r'\.proofpoint\.com$',
|
|
r'mail\.protection\.proofpoint\.com$',
|
|
]
|
|
|
|
PROOFPOINT_SPF_INCLUDES = [
|
|
'spf-a.proofpoint.com',
|
|
'spf-b.proofpoint.com',
|
|
'spf.proofpoint.com',
|
|
'pphosted.com',
|
|
]
|
|
|
|
PROOFPOINT_HEADER_MARKERS = [
|
|
'X-Proofpoint-Spam-Details',
|
|
'X-Proofpoint-Virus-Version',
|
|
'X-Proofpoint-GUID',
|
|
'X-Proofpoint-ORIG-GUID',
|
|
]
|
|
|
|
|
|
def check_mx_records(domain: str) -> MXCheckResult:
|
|
"""Check if domain MX records route through Proofpoint."""
|
|
result = MXCheckResult(domain=domain)
|
|
|
|
if not HAS_DNS:
|
|
result.issues.append("dnspython not installed. Install with: pip install dnspython")
|
|
return result
|
|
|
|
try:
|
|
answers = dns.resolver.resolve(domain, 'MX')
|
|
for rdata in sorted(answers, key=lambda r: r.preference):
|
|
mx_host = str(rdata.exchange).rstrip('.')
|
|
result.mx_records.append({
|
|
"priority": rdata.preference,
|
|
"host": mx_host
|
|
})
|
|
for pattern in PROOFPOINT_MX_PATTERNS:
|
|
if re.search(pattern, mx_host, re.IGNORECASE):
|
|
result.routes_through_proofpoint = True
|
|
result.proofpoint_mx = mx_host
|
|
break
|
|
except dns.resolver.NXDOMAIN:
|
|
result.issues.append(f"Domain {domain} does not exist")
|
|
except dns.resolver.NoAnswer:
|
|
result.issues.append(f"No MX records found for {domain}")
|
|
except Exception as e:
|
|
result.issues.append(f"DNS query failed: {str(e)}")
|
|
|
|
if not result.routes_through_proofpoint and result.mx_records:
|
|
result.issues.append(
|
|
"MX records do not point to Proofpoint. "
|
|
"Expected pattern: *.pphosted.com or *.proofpoint.com"
|
|
)
|
|
|
|
return result
|
|
|
|
|
|
def check_authentication(domain: str) -> AuthCheckResult:
|
|
"""Check SPF, DKIM, and DMARC records for Proofpoint alignment."""
|
|
result = AuthCheckResult(domain=domain)
|
|
|
|
if not HAS_DNS:
|
|
result.issues.append("dnspython not installed. Install with: pip install dnspython")
|
|
return result
|
|
|
|
# Check SPF
|
|
try:
|
|
answers = dns.resolver.resolve(domain, 'TXT')
|
|
for rdata in answers:
|
|
txt = str(rdata).strip('"')
|
|
if txt.startswith('v=spf1'):
|
|
result.spf_record = txt
|
|
for include in PROOFPOINT_SPF_INCLUDES:
|
|
if include in txt:
|
|
result.spf_includes_proofpoint = True
|
|
break
|
|
break
|
|
except Exception:
|
|
result.issues.append("Could not retrieve SPF record")
|
|
|
|
if result.spf_record and not result.spf_includes_proofpoint:
|
|
result.issues.append(
|
|
"SPF record does not include Proofpoint. "
|
|
"Add: include:spf-a.proofpoint.com"
|
|
)
|
|
|
|
# Check DMARC
|
|
try:
|
|
dmarc_domain = f"_dmarc.{domain}"
|
|
answers = dns.resolver.resolve(dmarc_domain, 'TXT')
|
|
for rdata in answers:
|
|
txt = str(rdata).strip('"')
|
|
if txt.startswith('v=DMARC1'):
|
|
result.dmarc_record = txt
|
|
policy_match = re.search(r'p=(\w+)', txt)
|
|
if policy_match:
|
|
result.dmarc_policy = policy_match.group(1)
|
|
break
|
|
except Exception:
|
|
result.issues.append("No DMARC record found")
|
|
|
|
if result.dmarc_policy == "none":
|
|
result.issues.append(
|
|
"DMARC policy is set to 'none' (monitoring only). "
|
|
"Plan rollout to 'quarantine' then 'reject'"
|
|
)
|
|
|
|
# Check Proofpoint DKIM selector
|
|
try:
|
|
dkim_domain = f"proofpoint._domainkey.{domain}"
|
|
answers = dns.resolver.resolve(dkim_domain, 'TXT')
|
|
for rdata in answers:
|
|
result.dkim_selector = "proofpoint"
|
|
break
|
|
except Exception:
|
|
pass
|
|
|
|
return result
|
|
|
|
|
|
def analyze_headers(eml_content: str) -> HeaderAnalysis:
|
|
"""Analyze email headers for Proofpoint routing and authentication."""
|
|
analysis = HeaderAnalysis()
|
|
|
|
# Extract From header
|
|
from_match = re.search(r'^From:\s*(.+)$', eml_content, re.MULTILINE | re.IGNORECASE)
|
|
if from_match:
|
|
analysis.from_address = from_match.group(1).strip()
|
|
|
|
# Extract Return-Path
|
|
rp_match = re.search(r'^Return-Path:\s*(.+)$', eml_content, re.MULTILINE | re.IGNORECASE)
|
|
if rp_match:
|
|
analysis.return_path = rp_match.group(1).strip()
|
|
|
|
# Extract Received chain
|
|
received_headers = re.findall(
|
|
r'^Received:\s*(.*?)(?=\n\S|\nReceived:|\n\n)',
|
|
eml_content, re.MULTILINE | re.DOTALL | re.IGNORECASE
|
|
)
|
|
for hdr in received_headers:
|
|
clean = ' '.join(hdr.split())
|
|
analysis.received_chain.append(clean)
|
|
if any(p.replace(r'$', '').replace(r'\.', '.') in clean.lower()
|
|
for p in ['pphosted.com', 'proofpoint.com']):
|
|
analysis.passed_through_proofpoint = True
|
|
|
|
# Extract Authentication-Results
|
|
auth_match = re.search(
|
|
r'^Authentication-Results:\s*(.*?)(?=\n\S)',
|
|
eml_content, re.MULTILINE | re.DOTALL | re.IGNORECASE
|
|
)
|
|
if auth_match:
|
|
auth_text = auth_match.group(1)
|
|
spf_match = re.search(r'spf=(\w+)', auth_text)
|
|
if spf_match:
|
|
analysis.spf_result = spf_match.group(1)
|
|
dkim_match = re.search(r'dkim=(\w+)', auth_text)
|
|
if dkim_match:
|
|
analysis.dkim_result = dkim_match.group(1)
|
|
dmarc_match = re.search(r'dmarc=(\w+)', auth_text)
|
|
if dmarc_match:
|
|
analysis.dmarc_result = dmarc_match.group(1)
|
|
|
|
# Check for Proofpoint-specific headers
|
|
for marker in PROOFPOINT_HEADER_MARKERS:
|
|
marker_match = re.search(
|
|
rf'^{re.escape(marker)}:\s*(.+)$',
|
|
eml_content, re.MULTILINE | re.IGNORECASE
|
|
)
|
|
if marker_match:
|
|
analysis.proofpoint_headers.append({
|
|
"header": marker,
|
|
"value": marker_match.group(1).strip()
|
|
})
|
|
if marker == 'X-Proofpoint-Spam-Details':
|
|
analysis.x_proofpoint_spam_details = marker_match.group(1).strip()
|
|
elif marker == 'X-Proofpoint-Virus-Version':
|
|
analysis.x_proofpoint_virus_version = marker_match.group(1).strip()
|
|
|
|
if not analysis.passed_through_proofpoint and not analysis.proofpoint_headers:
|
|
analysis.issues.append("Email does not appear to have routed through Proofpoint")
|
|
|
|
if analysis.spf_result and analysis.spf_result != 'pass':
|
|
analysis.issues.append(f"SPF check failed: {analysis.spf_result}")
|
|
if analysis.dkim_result and analysis.dkim_result != 'pass':
|
|
analysis.issues.append(f"DKIM check failed: {analysis.dkim_result}")
|
|
if analysis.dmarc_result and analysis.dmarc_result != 'pass':
|
|
analysis.issues.append(f"DMARC check failed: {analysis.dmarc_result}")
|
|
|
|
return analysis
|
|
|
|
|
|
def format_report(title: str, data: dict) -> str:
|
|
"""Format check results as a readable report."""
|
|
lines = []
|
|
lines.append("=" * 60)
|
|
lines.append(f" {title}")
|
|
lines.append("=" * 60)
|
|
|
|
for key, value in data.items():
|
|
if key == 'issues':
|
|
if value:
|
|
lines.append(f"\n [ISSUES]")
|
|
for i, issue in enumerate(value, 1):
|
|
lines.append(f" {i}. {issue}")
|
|
elif isinstance(value, list):
|
|
lines.append(f"\n {key}:")
|
|
for item in value:
|
|
if isinstance(item, dict):
|
|
lines.append(f" - {json.dumps(item)}")
|
|
else:
|
|
lines.append(f" - {item}")
|
|
elif isinstance(value, bool):
|
|
status = "YES" if value else "NO"
|
|
lines.append(f" {key}: {status}")
|
|
else:
|
|
lines.append(f" {key}: {value}")
|
|
|
|
lines.append("=" * 60)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="Proofpoint Email Gateway Validator")
|
|
subparsers = parser.add_subparsers(dest="command")
|
|
|
|
mx_parser = subparsers.add_parser("check-mx", help="Check MX records for Proofpoint routing")
|
|
mx_parser.add_argument("--domain", required=True, help="Domain to check")
|
|
|
|
auth_parser = subparsers.add_parser("check-auth", help="Check email authentication records")
|
|
auth_parser.add_argument("--domain", required=True, help="Domain to check")
|
|
|
|
hdr_parser = subparsers.add_parser("validate-headers", help="Analyze email headers")
|
|
hdr_parser.add_argument("--eml-file", required=True, help="Path to .eml file")
|
|
|
|
parser.add_argument("--json", action="store_true", help="Output as JSON")
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.command == "check-mx":
|
|
result = check_mx_records(args.domain)
|
|
if args.json:
|
|
print(json.dumps(asdict(result), indent=2))
|
|
else:
|
|
print(format_report("PROOFPOINT MX RECORD CHECK", asdict(result)))
|
|
|
|
elif args.command == "check-auth":
|
|
result = check_authentication(args.domain)
|
|
if args.json:
|
|
print(json.dumps(asdict(result), indent=2))
|
|
else:
|
|
print(format_report("EMAIL AUTHENTICATION CHECK", asdict(result)))
|
|
|
|
elif args.command == "validate-headers":
|
|
with open(args.eml_file, 'r', errors='replace') as f:
|
|
content = f.read()
|
|
result = analyze_headers(content)
|
|
if args.json:
|
|
print(json.dumps(asdict(result), indent=2))
|
|
else:
|
|
print(format_report("EMAIL HEADER ANALYSIS", asdict(result)))
|
|
|
|
else:
|
|
parser.print_help()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|