#!/usr/bin/env python3
"""
Crypto inventory scanner -- Chapter 10, Lab 6.

Scans a source tree for cryptographic primitive usage and emits a
CycloneDX-format Cryptographic Bill of Materials (CBOM).

Usage:
    python cbom_scanner.py [--target /path/to/scan] [--output cbom.json]

Output is a CycloneDX 1.4 JSON document with component type
"cryptographic-asset" for each detected usage, tagged with:
  - algorithm name
  - key size (if detectable from context)
  - usage context (key-exchange, signature, hashing, at-rest, tls-config)
  - quantum-vulnerable flag (True for RSA/ECDH/ECDSA/DH; False for AES-256/SHA-384+)
  - file and line number

Patterns are intentionally illustrative, not exhaustive.
"""

import argparse
import ast
import json
import os
import re
import sys
import uuid
from datetime import datetime, timezone
from typing import Any


# ---------------------------------------------------------------------------
# Detection patterns
# ---------------------------------------------------------------------------

# (regex, algorithm_name, usage_context, quantum_vulnerable, note)
PATTERNS: list[tuple[re.Pattern, str, str, bool, str]] = [
    # RSA variants
    (re.compile(r'\bRSA\b', re.I),            "RSA",      "key-exchange/signature", True,  "Verify key size; RSA-2048 deprecated after 2030 (NIST IR 8547)"),
    (re.compile(r'PKCS1',   re.I),            "RSA-PKCS1","signature/encryption",   True,  "PKCS1-v1.5 vulnerable to Bleichenbacher oracle; prefer OAEP/PSS"),
    (re.compile(r'PKCS#1',  re.I),            "RSA-PKCS1","signature/encryption",   True,  "PKCS1-v1.5 vulnerable to Bleichenbacher oracle; prefer OAEP/PSS"),
    # ECDSA / ECDH
    (re.compile(r'\bECDSA\b',    re.I),       "ECDSA",    "signature",             True,  "Deprecated after 2035 (NIST IR 8547); consider ML-DSA migration"),
    (re.compile(r'\bECDH\b',     re.I),       "ECDH",     "key-exchange",          True,  "Deprecated after 2035; priority HNDL target; migrate to ML-KEM"),
    (re.compile(r'\bsecp256r1\b|\bP-256\b',re.I),"P-256/secp256r1","key-exchange/signature",True,"128-bit classical; 0 quantum bits"),
    (re.compile(r'\bsecp384r1\b|\bP-384\b',re.I),"P-384/secp384r1","key-exchange/signature",True,"192-bit classical; 0 quantum bits"),
    # DH / FFDH
    (re.compile(r'\bDiffie.Hellman\b|\bDH\b', re.I),"FFDH","key-exchange",         True,  "Deprecated after 2035; migrate to ML-KEM for key establishment"),
    # AES variants
    (re.compile(r'\bAES.?128\b',  re.I),      "AES-128",  "encryption",            False, "Grover reduces to 64-bit effective; prefer AES-256 for PQC safety"),
    (re.compile(r'\bAES.?256\b',  re.I),      "AES-256",  "encryption",            False, "Quantum-safe (Grover halves to 128-bit effective) -- keep"),
    (re.compile(r'\bAES.?CBC\b',  re.I),      "AES-CBC",  "encryption",            False, "No authentication; prefer AES-GCM (AEAD)"),
    (re.compile(r'\bAES.?GCM\b',  re.I),      "AES-GCM",  "encryption",            False, "AEAD -- preferred mode"),
    (re.compile(r'\b3DES\b|\bDES\b', re.I),   "3DES/DES", "encryption",            False, "Disallowed (NIST SP 800-131A); replace immediately"),
    # Hash algorithms
    (re.compile(r'\bMD5\b',    re.I),         "MD5",      "hashing",               False, "Broken for collision resistance; disallowed for security use (NIST SP 800-131A)"),
    (re.compile(r'\bSHA.?1\b', re.I),         "SHA-1",    "hashing",               False, "Deprecated (NIST SP 800-131A); migrate to SHA-256+"),
    (re.compile(r'\bSHA.?256\b',re.I),        "SHA-256",  "hashing",               False, "Quantum-safe (Grover halves to 128-bit) -- acceptable"),
    (re.compile(r'\bSHA.?384\b',re.I),        "SHA-384",  "hashing",               False, "Quantum-safe -- preferred"),
    (re.compile(r'\bSHA.?512\b',re.I),        "SHA-512",  "hashing",               False, "Quantum-safe -- preferred"),
    # TLS versions
    (re.compile(r'\bTLSv?1\.0\b',re.I),       "TLS 1.0",  "tls-config",           False, "Disallowed; remove immediately"),
    (re.compile(r'\bTLSv?1\.1\b',re.I),       "TLS 1.1",  "tls-config",           False, "Disallowed; remove immediately"),
    (re.compile(r'\bTLSv?1\.2\b',re.I),       "TLS 1.2",  "tls-config",           False, "Acceptable if restricted to AEAD+ECDHE; plan migration to TLS 1.3"),
    (re.compile(r'\bTLSv?1\.3\b',re.I),       "TLS 1.3",  "tls-config",           False, "Preferred -- forward-secret by construction"),
]

SKIP_DIRS = {".git", "__pycache__", "node_modules", ".venv", "venv"}
TEXT_EXTENSIONS = {
    ".py", ".js", ".ts", ".java", ".go", ".rb", ".c", ".cpp", ".h",
    ".rs", ".php", ".kt", ".swift", ".cs", ".scala",
    ".tf", ".hcl", ".yaml", ".yml", ".json", ".toml", ".ini", ".cfg", ".conf",
    ".md", ".txt",
}


def scan_file(filepath: str) -> list[dict[str, Any]]:
    findings: list[dict[str, Any]] = []
    try:
        with open(filepath, encoding="utf-8", errors="ignore") as fh:
            for lineno, line in enumerate(fh, start=1):
                for pattern, alg, usage, q_vuln, note in PATTERNS:
                    if pattern.search(line):
                        findings.append({
                            "file": filepath,
                            "line": lineno,
                            "algorithm": alg,
                            "usage": usage,
                            "quantum_vulnerable": q_vuln,
                            "note": note,
                            "snippet": line.rstrip()[:120],
                        })
    except OSError:
        pass
    return findings


def scan_directory(target: str) -> list[dict[str, Any]]:
    all_findings: list[dict[str, Any]] = []
    for root, dirs, files in os.walk(target):
        dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
        for fname in files:
            ext = os.path.splitext(fname)[1].lower()
            if ext in TEXT_EXTENSIONS:
                full_path = os.path.join(root, fname)
                all_findings.extend(scan_file(full_path))
    return all_findings


def build_cbom(findings: list[dict[str, Any]], target: str) -> dict[str, Any]:
    """Emit a CycloneDX 1.4 JSON CBOM."""
    components = []
    for i, f in enumerate(findings):
        components.append({
            "type": "cryptographic-asset",
            "bom-ref": f"crypto-{i+1}",
            "name": f["algorithm"],
            "cryptoProperties": {
                "assetType": "algorithm",
                "algorithmProperties": {
                    "primitive": f["usage"],
                    "parameterSetIdentifier": f["algorithm"],
                },
                "quantumVulnerable": f["quantum_vulnerable"],
            },
            "evidence": {
                "occurrences": [
                    {
                        "location": f["file"],
                        "line": f["line"],
                    }
                ]
            },
            "description": f["note"],
            "externalReferences": [],
        })

    return {
        "bomFormat": "CycloneDX",
        "specVersion": "1.4",
        "version": 1,
        "serialNumber": f"urn:uuid:{uuid.uuid4()}",
        "metadata": {
            "timestamp": datetime.now(timezone.utc).isoformat(),
            "component": {
                "type": "library",
                "name": target,
                "description": "Scanned by Chapter 10 Lab 6 crypto inventory scanner",
            },
        },
        "components": components,
    }


def prioritise(findings: list[dict[str, Any]]) -> list[dict[str, Any]]:
    """
    Sort findings by quantum risk and confidentiality lifetime.

    Priority order (highest first):
      1. Quantum-vulnerable key-exchange (HNDL target -- harvest now, decrypt later)
      2. Quantum-vulnerable signatures
      3. Deprecated/broken symmetric primitives (DES, MD5, SHA-1, TLS 1.0/1.1)
      4. Everything else
    """
    def priority_key(f):
        q = f["quantum_vulnerable"]
        u = f["usage"]
        if q and "key-exchange" in u:
            return 0
        if q and "signature" in u:
            return 1
        if f["algorithm"] in ("3DES/DES", "MD5", "SHA-1", "TLS 1.0", "TLS 1.1"):
            return 2
        return 3

    return sorted(findings, key=priority_key)


def print_migration_table(prioritised: list[dict[str, Any]]) -> None:
    print("\n=== Migration Priority Table ===")
    print(f"{'Pri':>3}  {'Algorithm':<22} {'Usage':<28} {'Q-Vuln':>6}  Location")
    print("-" * 100)
    prev_pri = -1
    labels = {0: "CRITICAL (HNDL)", 1: "HIGH (sig, PQC)", 2: "URGENT (broken)", 3: "REVIEW"}
    for f in prioritised:
        q = f["quantum_vulnerable"]
        u = f["usage"]
        if q and "key-exchange" in u:
            pri = 0
        elif q and "signature" in u:
            pri = 1
        elif f["algorithm"] in ("3DES/DES", "MD5", "SHA-1", "TLS 1.0", "TLS 1.1"):
            pri = 2
        else:
            pri = 3
        if pri != prev_pri:
            print(f"\n  [{labels[pri]}]")
            prev_pri = pri
        loc = f"{f['file']}:{f['line']}"[-50:]
        print(f"  {pri:>3}  {f['algorithm']:<22} {f['usage']:<28} {'YES' if q else 'no':>6}  {loc}")


def main():
    parser = argparse.ArgumentParser(description="Chapter 10 Lab 6: Crypto inventory scanner")
    parser.add_argument("--target", default="/scan", help="Directory to scan")
    parser.add_argument("--output", default="/output/cbom.json", help="CBOM output path")
    args = parser.parse_args()

    print(f"[*] Scanning: {args.target}")
    raw = scan_directory(args.target)
    print(f"[*] Found {len(raw)} raw findings")

    prioritised = prioritise(raw)
    print_migration_table(prioritised)

    cbom = build_cbom(prioritised, args.target)
    os.makedirs(os.path.dirname(args.output), exist_ok=True)
    with open(args.output, "w") as fh:
        json.dump(cbom, fh, indent=2)
    print(f"\n[+] CBOM written to {args.output} ({len(cbom['components'])} components)")
    print("[*] Next step: open cbom.json and build the migration plan (Lab 6 step 4)")


if __name__ == "__main__":
    main()
