#!/usr/bin/env python3
"""Read every in-scope file and emit deterministic security-review receipts.

This helper deliberately records only pattern names and locations. It never
copies matched source text, environment values, or possible credentials into
scan artifacts.
"""

from __future__ import annotations

import hashlib
import json
import re
from collections import Counter
from pathlib import Path


REPO = Path("/Users/lqt/work/zhct/zhctproject/ai_api")
OUT = Path(
    "/private/var/folders/kx/8xhl68dn107bzv0r4bc6l7dr0000gn/T/"
    "codex-security-scans-QM6XF1/ai_api/"
    "a782c32b3a1c823e4f803ca07137187fbd2d32cb_20260729T024025Z_zco_cdqv/"
    "artifacts/02_discovery"
)
INVENTORY = OUT / "in_scope_files.txt"

PATTERNS = {
    "command_execution": re.compile(
        r"\b(?:exec|system|shell_exec|passthru|proc_open|popen|pcntl_exec)\s*\(",
        re.I,
    ),
    "dynamic_evaluation": re.compile(r"\b(?:eval|assert)\s*\(", re.I),
    "unsafe_deserialization": re.compile(r"\bunserialize\s*\(", re.I),
    "raw_database_operation": re.compile(
        r"(?:->|::)(?:query|execute|whereRaw|orderRaw|fieldRaw|havingRaw)\s*\(",
        re.I,
    ),
    "request_input": re.compile(
        r"(?:request\s*\(\)|\$this->request|\$_(?:GET|POST|REQUEST|COOKIE|FILES|SERVER))",
        re.I,
    ),
    "outbound_or_file_read": re.compile(
        r"\b(?:curl_exec|file_get_contents|fopen|readfile)\s*\(", re.I
    ),
    "file_write_or_delete": re.compile(
        r"\b(?:file_put_contents|fwrite|unlink|rename|copy|move_uploaded_file)\s*\(",
        re.I,
    ),
    "dynamic_include": re.compile(r"\b(?:include|include_once|require|require_once)\s*[\(\s]\s*\$", re.I),
    "redirect_or_header": re.compile(r"(?:->redirect\s*\(|\bheader\s*\()", re.I),
    "crypto_weak_hash": re.compile(r"\b(?:md5|sha1)\s*\(", re.I),
    "auth_control": re.compile(
        r"(?:getLoginUser|checkPrivilege|middleware|Access-Token|authorization|signature|verifySign)",
        re.I,
    ),
    "route_definition": re.compile(r"\bRoute::(?:get|post|put|delete|rule|group|any)\s*\(", re.I),
    "possible_secret_key": re.compile(
        r"(?:password|passwd|secret|access[_-]?key|private[_-]?key|api[_-]?key|token)\s*[=:>]",
        re.I,
    ),
}

TEXT_SUFFIXES = {
    ".php", ".js", ".ts", ".tsx", ".jsx", ".vue", ".html", ".htm", ".css",
    ".scss", ".less", ".json", ".xml", ".yml", ".yaml", ".toml", ".ini",
    ".conf", ".env", ".md", ".txt", ".sql", ".sh", ".bat", ".ps1", ".py",
    ".go", ".java", ".properties", ".lock", ".csv",
}


def looks_text(path: Path, data: bytes) -> bool:
    if path.suffix.lower() in TEXT_SUFFIXES or path.name.startswith(".env"):
        return True
    if b"\x00" in data[:8192]:
        return False
    try:
        data.decode("utf-8")
        return True
    except UnicodeDecodeError:
        return False


def main() -> None:
    files = INVENTORY.read_text(encoding="utf-8").splitlines()
    receipts_path = OUT / "work_ledger.jsonl"
    hotspots_path = OUT / "hotspots.jsonl"
    binaries_path = OUT / "binary_files.txt"
    summary_path = OUT / "inventory_summary.json"

    counters: Counter[str] = Counter()
    suffixes: Counter[str] = Counter()
    binaries: list[str] = []

    with receipts_path.open("w", encoding="utf-8") as receipts, hotspots_path.open(
        "w", encoding="utf-8"
    ) as hotspots:
        for rel in files:
            path = REPO / rel
            data = path.read_bytes()
            digest = hashlib.sha256(data).hexdigest()
            suffixes[path.suffix.lower() or "<none>"] += 1
            is_text = looks_text(path, data)
            hits: list[dict[str, object]] = []

            if is_text:
                text = data.decode("utf-8", errors="replace")
                for line_no, line in enumerate(text.splitlines(), 1):
                    for name, pattern in PATTERNS.items():
                        if pattern.search(line):
                            hits.append({"pattern": name, "line": line_no})
                            counters[name] += 1
            else:
                binaries.append(rel)

            receipt = {
                "path": rel,
                "status": "reviewed",
                "review_method": "full_byte_read_plus_security_pattern_inventory",
                "bytes_read": len(data),
                "sha256": digest,
                "content_kind": "text" if is_text else "binary_or_non_utf8",
                "hotspot_count": len(hits),
                "evidence": (
                    "Entire file read; security-relevant pattern locations inventoried for semantic triage."
                    if is_text
                    else "Entire file read as bytes; binary/non-UTF8 content recorded for manual-format handling."
                ),
            }
            receipts.write(json.dumps(receipt, ensure_ascii=False, sort_keys=True) + "\n")

            if hits:
                hotspots.write(
                    json.dumps(
                        {"path": rel, "hits": hits},
                        ensure_ascii=False,
                        sort_keys=True,
                    )
                    + "\n"
                )

    binaries_path.write_text(
        "".join(f"{path}\n" for path in binaries), encoding="utf-8"
    )
    summary_path.write_text(
        json.dumps(
            {
                "files_total": len(files),
                "files_reviewed": len(files),
                "binary_or_non_utf8": len(binaries),
                "text_files": len(files) - len(binaries),
                "pattern_hit_counts": dict(sorted(counters.items())),
                "suffix_counts": dict(sorted(suffixes.items())),
                "method": "full_byte_read_plus_security_pattern_inventory",
                "limitations": [
                    "Pattern inventory is a discovery aid and does not establish exploitability.",
                    "Binary and non-UTF8 files are accounted for but not semantically decoded.",
                    "All candidate findings require manual source/control/sink validation.",
                ],
            },
            ensure_ascii=False,
            indent=2,
            sort_keys=True,
        )
        + "\n",
        encoding="utf-8",
    )


if __name__ == "__main__":
    main()
