#!/usr/bin/env python3 """PCI DSS compliance control audit agent. Audits systems and configurations against PCI DSS v4.0 requirements including network segmentation, encryption, access controls, logging, vulnerability management, and secure configuration checks. """ import argparse import json import os import re import socket import ssl import subprocess import sys from datetime import datetime, timezone def check_tls_configuration(host, port=443): """PCI DSS Req 4.2.1 - Strong cryptography for transmission.""" findings = [] print(f"[*] Req 4.2.1: Checking TLS on {host}:{port}") try: context = ssl.create_default_context() with socket.create_connection((host, port), timeout=10) as sock: with context.wrap_socket(sock, server_hostname=host) as ssock: protocol = ssock.version() cipher = ssock.cipher() if protocol in ("TLSv1.0", "TLSv1.1", "SSLv3", "SSLv2"): findings.append({ "requirement": "4.2.1", "check": "TLS Protocol Version", "status": "FAIL", "severity": "CRITICAL", "detail": f"Deprecated protocol: {protocol}", }) else: findings.append({ "requirement": "4.2.1", "check": "TLS Protocol Version", "status": "PASS", "severity": "INFO", "detail": f"Protocol: {protocol}", }) if cipher: weak_ciphers = ["RC4", "DES", "3DES", "NULL", "EXPORT", "MD5"] if any(w in cipher[0] for w in weak_ciphers): findings.append({ "requirement": "4.2.1", "check": "Cipher Strength", "status": "FAIL", "severity": "HIGH", "detail": f"Weak cipher: {cipher[0]}", }) else: findings.append({ "requirement": "4.2.1", "check": "Cipher Strength", "status": "PASS", "severity": "INFO", "detail": f"Cipher: {cipher[0]} ({cipher[2]} bits)", }) except Exception as e: findings.append({ "requirement": "4.2.1", "check": "TLS Connection", "status": "ERROR", "severity": "HIGH", "detail": str(e)[:100], }) return findings def check_password_policy(): """PCI DSS Req 8.3.6 - Password complexity requirements.""" findings = [] print("[*] Req 8.3.6: Checking password policy") if sys.platform != "win32": # Check PAM password quality pam_files = ["/etc/pam.d/common-password", "/etc/pam.d/system-auth", "/etc/security/pwquality.conf"] for pam_file in pam_files: if os.path.isfile(pam_file): with open(pam_file, "r") as f: content = f.read() if "minlen" in content: match = re.search(r'minlen\s*=\s*(\d+)', content) if match and int(match.group(1)) >= 12: findings.append({ "requirement": "8.3.6", "check": "Min password length", "status": "PASS", "severity": "INFO", "detail": f"minlen={match.group(1)} in {pam_file}", }) else: findings.append({ "requirement": "8.3.6", "check": "Min password length", "status": "FAIL", "severity": "HIGH", "detail": f"Password minlen < 12 in {pam_file}", }) break else: findings.append({ "requirement": "8.3.6", "check": "Password policy config", "status": "WARN", "severity": "MEDIUM", "detail": "Could not find PAM password config", }) return findings def check_audit_logging(): """PCI DSS Req 10.2 - Audit logging configuration.""" findings = [] print("[*] Req 10.2: Checking audit logging") if sys.platform != "win32": # Check auditd result = subprocess.run( ["systemctl", "is-active", "auditd"], capture_output=True, text=True, timeout=10, ) if result.stdout.strip() == "active": findings.append({ "requirement": "10.2", "check": "Audit daemon running", "status": "PASS", "severity": "INFO", }) else: findings.append({ "requirement": "10.2", "check": "Audit daemon running", "status": "FAIL", "severity": "CRITICAL", "detail": "auditd is not running", }) # Check syslog for syslog in ["rsyslog", "syslog-ng"]: result = subprocess.run( ["systemctl", "is-active", syslog], capture_output=True, text=True, timeout=10, ) if result.stdout.strip() == "active": findings.append({ "requirement": "10.2", "check": f"{syslog} running", "status": "PASS", "severity": "INFO", }) break return findings def check_file_integrity(): """PCI DSS Req 11.5.2 - File integrity monitoring.""" findings = [] print("[*] Req 11.5.2: Checking file integrity monitoring") fim_tools = { "aide": ["/usr/bin/aide", "/usr/sbin/aide"], "ossec": ["/var/ossec/bin/ossec-syscheckd"], "tripwire": ["/usr/sbin/tripwire"], "samhain": ["/usr/local/sbin/samhain"], } found_fim = False for tool_name, paths in fim_tools.items(): for path in paths: if os.path.isfile(path): findings.append({ "requirement": "11.5.2", "check": f"FIM tool: {tool_name}", "status": "PASS", "severity": "INFO", "detail": f"Found at {path}", }) found_fim = True break if not found_fim: findings.append({ "requirement": "11.5.2", "check": "File integrity monitoring", "status": "FAIL", "severity": "HIGH", "detail": "No FIM tool detected (AIDE, OSSEC, Tripwire, Samhain)", }) return findings def check_default_credentials(): """PCI DSS Req 2.2.2 - Change vendor defaults.""" findings = [] print("[*] Req 2.2.2: Checking for default credentials") # Check for common default accounts if os.path.isfile("/etc/passwd"): with open("/etc/passwd", "r") as f: for line in f: parts = line.strip().split(":") if len(parts) >= 7: username = parts[0] shell = parts[6] if username in ("guest", "test", "demo", "admin") and shell not in ("/usr/sbin/nologin", "/bin/false"): findings.append({ "requirement": "2.2.2", "check": f"Default account: {username}", "status": "FAIL", "severity": "HIGH", "detail": f"Account '{username}' has login shell: {shell}", }) return findings def check_network_segmentation(target_ip, ports=None): """PCI DSS Req 1.3 - Network segmentation check.""" findings = [] if not ports: ports = [22, 80, 443, 3306, 5432, 1433, 6379, 9200, 27017] print(f"[*] Req 1.3: Checking network segmentation to {target_ip}") for port in ports: try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(3) result = sock.connect_ex((target_ip, port)) sock.close() if result == 0: findings.append({ "requirement": "1.3", "check": f"Port {port} reachable", "status": "WARN", "severity": "MEDIUM", "detail": f"{target_ip}:{port} is open from this network segment", }) except Exception: pass if not findings: findings.append({ "requirement": "1.3", "check": "Network segmentation", "status": "PASS", "severity": "INFO", "detail": f"No tested ports reachable on {target_ip}", }) return findings def format_summary(all_findings): """Print PCI DSS audit summary.""" print(f"\n{'='*60}") print(f" PCI DSS v4.0 Compliance Audit Report") print(f"{'='*60}") pass_count = sum(1 for f in all_findings if f["status"] == "PASS") fail_count = sum(1 for f in all_findings if f["status"] == "FAIL") warn_count = sum(1 for f in all_findings if f["status"] == "WARN") print(f" Total Checks : {len(all_findings)}") print(f" Passed : {pass_count}") print(f" Failed : {fail_count}") print(f" Warnings : {warn_count}") by_req = {} for f in all_findings: req = f.get("requirement", "unknown") by_req.setdefault(req, []).append(f) print(f"\n Results by Requirement:") for req in sorted(by_req.keys()): items = by_req[req] failed = sum(1 for i in items if i["status"] == "FAIL") passed = sum(1 for i in items if i["status"] == "PASS") status = "FAIL" if failed > 0 else "PASS" print(f" Req {req:8s}: [{status:4s}] {passed} passed, {failed} failed") if fail_count > 0: print(f"\n Failed Checks:") for f in all_findings: if f["status"] == "FAIL": print(f" [{f['severity']:8s}] Req {f['requirement']}: {f['check']} - {f.get('detail', '')}") severity_counts = {} for f in all_findings: if f["status"] == "FAIL": sev = f.get("severity", "MEDIUM") severity_counts[sev] = severity_counts.get(sev, 0) + 1 return severity_counts def main(): parser = argparse.ArgumentParser(description="PCI DSS compliance control audit agent") parser.add_argument("--tls-host", help="Host to check TLS configuration") parser.add_argument("--tls-port", type=int, default=443) parser.add_argument("--segment-target", help="IP to check network segmentation") parser.add_argument("--skip-password", action="store_true") parser.add_argument("--skip-logging", action="store_true") parser.add_argument("--skip-fim", action="store_true") parser.add_argument("--output", "-o", help="Output JSON report") parser.add_argument("--verbose", "-v", action="store_true") args = parser.parse_args() all_findings = [] if args.tls_host: all_findings.extend(check_tls_configuration(args.tls_host, args.tls_port)) if not args.skip_password: all_findings.extend(check_password_policy()) if not args.skip_logging: all_findings.extend(check_audit_logging()) if not args.skip_fim: all_findings.extend(check_file_integrity()) all_findings.extend(check_default_credentials()) if args.segment_target: all_findings.extend(check_network_segmentation(args.segment_target)) if not all_findings: print("[!] No checks performed. Use --tls-host or other options.", file=sys.stderr) sys.exit(1) severity_counts = format_summary(all_findings) report = { "timestamp": datetime.now(timezone.utc).isoformat(), "tool": "PCI DSS Audit", "standard": "PCI DSS v4.0", "findings": all_findings, "severity_counts": severity_counts, "risk_level": ( "CRITICAL" if severity_counts.get("CRITICAL", 0) > 0 else "HIGH" if severity_counts.get("HIGH", 0) > 0 else "MEDIUM" if severity_counts.get("MEDIUM", 0) > 0 else "LOW" ), } if args.output: with open(args.output, "w") as f: json.dump(report, f, indent=2) print(f"\n[+] Report saved to {args.output}") elif args.verbose: print(json.dumps(report, indent=2)) if __name__ == "__main__": main()