#!/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()