#!/usr/bin/env python3
"""Application cryptographic audit agent.

Audits application code and configurations for cryptographic weaknesses
including weak algorithms, insecure key sizes, hardcoded keys, deprecated
protocols, and missing certificate validation. Scans source files, config
files, and TLS endpoints.
"""
import argparse
import json
import os
import re
import ssl
import socket
import sys
from datetime import datetime, timezone

try:
    from cryptography import x509
    from cryptography.hazmat.backends import default_backend
    HAS_CRYPTO = True
except ImportError:
    HAS_CRYPTO = False


WEAK_PATTERNS = {
    "weak_hash": {
        "pattern": r'\b(MD5|md5|SHA1|sha1|SHA-1)\b(?!.*hmac)',
        "severity": "HIGH",
        "description": "Weak hash algorithm detected (MD5/SHA1)",
        "recommendation": "Use SHA-256 or SHA-3",
    },
    "weak_cipher": {
        "pattern": r'\b(DES|3DES|RC4|RC2|Blowfish|IDEA)\b',
        "severity": "HIGH",
        "description": "Weak cipher algorithm detected",
        "recommendation": "Use AES-256-GCM or ChaCha20-Poly1305",
    },
    "ecb_mode": {
        "pattern": r'\b(ECB|MODE_ECB|ecb)\b',
        "severity": "CRITICAL",
        "description": "ECB mode detected - does not provide semantic security",
        "recommendation": "Use GCM, CBC with HMAC, or CTR mode",
    },
    "hardcoded_key": {
        "pattern": r'(?:key|secret|password|token)\s*[=:]\s*["\'][A-Za-z0-9+/=]{16,}["\']',
        "severity": "CRITICAL",
        "description": "Possible hardcoded cryptographic key or secret",
        "recommendation": "Use environment variables or a secrets manager",
    },
    "small_rsa_key": {
        "pattern": r'(?:key_?size|bits)\s*[=:]\s*(?:512|768|1024)\b',
        "severity": "CRITICAL",
        "description": "RSA key size too small (<2048 bits)",
        "recommendation": "Use minimum 2048-bit, preferably 4096-bit RSA keys",
    },
    "weak_random": {
        "pattern": r'\b(?:random\.random|math\.random|Math\.random|rand\(\)|srand)\b',
        "severity": "HIGH",
        "description": "Non-cryptographic random number generator used",
        "recommendation": "Use secrets module, os.urandom, or crypto.getRandomValues",
    },
    "ssl_no_verify": {
        "pattern": r'(?:verify\s*=\s*False|CERT_NONE|SSL_VERIFY_NONE|InsecureRequestWarning|verify_ssl\s*=\s*False)',
        "severity": "HIGH",
        "description": "TLS certificate verification disabled",
        "recommendation": "Enable certificate verification in all TLS connections",
    },
    "tls_v1": {
        "pattern": r'(?:TLSv1[^._23]|SSLv[23]|PROTOCOL_TLS(?!v1_[23])|TLS_1_0|TLS_1_1)',
        "severity": "HIGH",
        "description": "Deprecated TLS/SSL protocol version detected",
        "recommendation": "Use TLS 1.2 or TLS 1.3 minimum",
    },
    "padding_oracle": {
        "pattern": r'(?:PKCS1v15|pkcs1_v1_5|PKCS5Padding)(?!.*OAEP)',
        "severity": "MEDIUM",
        "description": "PKCS#1 v1.5 padding used (vulnerable to padding oracle attacks)",
        "recommendation": "Use OAEP padding for RSA, or switch to AEAD ciphers",
    },
    "static_iv": {
        "pattern": r'(?:iv|IV|nonce)\s*[=:]\s*(?:b?["\'][^"\']{1,32}["\']|bytes\([^)]*\))',
        "severity": "HIGH",
        "description": "Possible static/hardcoded IV or nonce",
        "recommendation": "Generate random IV/nonce for each encryption operation",
    },
}

SCAN_EXTENSIONS = {
    ".py", ".js", ".ts", ".java", ".go", ".rb", ".php", ".cs", ".c", ".cpp",
    ".h", ".yaml", ".yml", ".json", ".xml", ".conf", ".cfg", ".ini", ".env",
    ".toml", ".properties",
}


def scan_file(file_path):
    """Scan a single file for cryptographic weaknesses."""
    findings = []
    try:
        with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
            lines = f.readlines()
    except (IOError, PermissionError):
        return findings

    for line_num, line in enumerate(lines, 1):
        for rule_id, rule in WEAK_PATTERNS.items():
            if re.search(rule["pattern"], line):
                findings.append({
                    "rule": rule_id,
                    "file": file_path,
                    "line": line_num,
                    "content": line.strip()[:120],
                    "severity": rule["severity"],
                    "description": rule["description"],
                    "recommendation": rule["recommendation"],
                })
    return findings


def scan_directory(directory, extensions=None, exclude_dirs=None):
    """Recursively scan a directory for cryptographic weaknesses."""
    if extensions is None:
        extensions = SCAN_EXTENSIONS
    if exclude_dirs is None:
        exclude_dirs = {"node_modules", ".git", "__pycache__", "venv", ".venv",
                        "vendor", "dist", "build", ".tox"}

    all_findings = []
    files_scanned = 0

    for root, dirs, files in os.walk(directory):
        dirs[:] = [d for d in dirs if d not in exclude_dirs]
        for fname in files:
            ext = os.path.splitext(fname)[1].lower()
            if ext not in extensions:
                continue
            full_path = os.path.join(root, fname)
            findings = scan_file(full_path)
            all_findings.extend(findings)
            files_scanned += 1

    print(f"[+] Scanned {files_scanned} files, found {len(all_findings)} issues")
    return all_findings, files_scanned


def audit_tls_endpoint(host, port=443):
    """Audit TLS configuration of a remote endpoint."""
    findings = []
    print(f"[*] Auditing TLS: {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:
                cert_der = ssock.getpeercert(binary_form=True)
                cipher = ssock.cipher()
                protocol = ssock.version()

                findings.append({
                    "check": "TLS Protocol",
                    "value": protocol,
                    "severity": "CRITICAL" if "TLSv1.0" in protocol or "TLSv1.1" in protocol
                               else "INFO",
                    "description": f"Negotiated protocol: {protocol}",
                })

                if cipher:
                    cipher_name, tls_ver, key_bits = cipher
                    findings.append({
                        "check": "Cipher Suite",
                        "value": cipher_name,
                        "severity": "HIGH" if any(w in cipher_name for w in ["RC4", "DES", "NULL", "EXPORT"])
                                   else "INFO",
                        "description": f"Cipher: {cipher_name} ({key_bits} bits)",
                    })

                if HAS_CRYPTO and cert_der:
                    cert = x509.load_der_x509_certificate(cert_der, default_backend())
                    not_after = cert.not_valid_after_utc if hasattr(cert, 'not_valid_after_utc') else cert.not_valid_after
                    days_remaining = (not_after - datetime.now(timezone.utc)).days

                    pub_key = cert.public_key()
                    key_size = getattr(pub_key, 'key_size', 0)
                    sig_algo = cert.signature_algorithm_oid._name if hasattr(cert.signature_algorithm_oid, '_name') else str(cert.signature_hash_algorithm)

                    findings.append({
                        "check": "Certificate Expiry",
                        "value": f"{days_remaining} days",
                        "severity": "CRITICAL" if days_remaining < 0
                                   else "HIGH" if days_remaining < 30
                                   else "MEDIUM" if days_remaining < 90
                                   else "INFO",
                        "description": f"Expires: {not_after.isoformat()} ({days_remaining} days)",
                    })
                    findings.append({
                        "check": "Key Size",
                        "value": f"{key_size} bits",
                        "severity": "CRITICAL" if key_size < 2048
                                   else "MEDIUM" if key_size < 4096
                                   else "INFO",
                    })
                    findings.append({
                        "check": "Signature Algorithm",
                        "value": str(sig_algo),
                        "severity": "HIGH" if "sha1" in str(sig_algo).lower() else "INFO",
                    })

    except ssl.SSLError as e:
        findings.append({"check": "TLS Connection", "severity": "CRITICAL",
                         "description": f"SSL error: {e}"})
    except socket.timeout:
        findings.append({"check": "TLS Connection", "severity": "HIGH",
                         "description": "Connection timed out"})
    except Exception as e:
        findings.append({"check": "TLS Connection", "severity": "HIGH",
                         "description": f"Error: {e}"})

    return findings


def format_summary(code_findings, tls_findings, files_scanned, target):
    """Print audit summary."""
    all_findings = code_findings + tls_findings
    print(f"\n{'='*60}")
    print(f"  Cryptographic Audit Report")
    print(f"{'='*60}")
    print(f"  Target         : {target}")
    print(f"  Files Scanned  : {files_scanned}")
    print(f"  Code Findings  : {len(code_findings)}")
    print(f"  TLS Findings   : {len(tls_findings)}")

    severity_counts = {}
    for f in all_findings:
        sev = f.get("severity", "INFO")
        severity_counts[sev] = severity_counts.get(sev, 0) + 1

    print(f"\n  By Severity:")
    for sev in ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]:
        count = severity_counts.get(sev, 0)
        if count > 0:
            print(f"    {sev:10s}: {count}")

    if code_findings:
        by_rule = {}
        for f in code_findings:
            by_rule.setdefault(f["rule"], []).append(f)
        print(f"\n  Code Issues by Rule:")
        for rule, items in sorted(by_rule.items(), key=lambda x: -len(x[1])):
            print(f"    {rule:25s}: {len(items)} ({items[0]['severity']})")

        print(f"\n  Top Code Findings:")
        for f in code_findings[:15]:
            if f["severity"] in ("CRITICAL", "HIGH"):
                print(f"    [{f['severity']:8s}] {f['file']}:{f['line']} - {f['description']}")

    if tls_findings:
        print(f"\n  TLS Audit Results:")
        for f in tls_findings:
            print(f"    [{f['severity']:8s}] {f.get('check', '')}: "
                  f"{f.get('value', f.get('description', ''))}")

    return severity_counts


def main():
    parser = argparse.ArgumentParser(
        description="Application cryptographic audit agent"
    )
    parser.add_argument("--target", required=True,
                        help="Source directory to scan or TLS host to audit")
    parser.add_argument("--tls-port", type=int, default=443,
                        help="TLS port for endpoint audit (default: 443)")
    parser.add_argument("--tls-only", action="store_true",
                        help="Only audit TLS endpoint, skip code scan")
    parser.add_argument("--code-only", action="store_true",
                        help="Only scan code, skip TLS audit")
    parser.add_argument("--exclude-dirs", nargs="+",
                        help="Additional directories to exclude from scan")
    parser.add_argument("--output", "-o", help="Output JSON report path")
    parser.add_argument("--verbose", "-v", action="store_true")
    args = parser.parse_args()

    code_findings = []
    tls_findings = []
    files_scanned = 0

    if not args.tls_only and os.path.isdir(args.target):
        exclude = {"node_modules", ".git", "__pycache__", "venv", ".venv", "vendor"}
        if args.exclude_dirs:
            exclude.update(args.exclude_dirs)
        code_findings, files_scanned = scan_directory(args.target, exclude_dirs=exclude)

    if not args.code_only and not os.path.isdir(args.target):
        tls_findings = audit_tls_endpoint(args.target, args.tls_port)
    elif not args.code_only and os.path.isdir(args.target):
        pass  # Can't audit TLS on a directory

    severity_counts = format_summary(code_findings, tls_findings, files_scanned, args.target)

    report = {
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "tool": "Crypto Audit",
        "target": args.target,
        "files_scanned": files_scanned,
        "severity_counts": severity_counts,
        "code_findings": code_findings,
        "tls_findings": tls_findings,
        "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()
