#!/usr/bin/env python3
"""
verify-artifact -- standalone sanitization assertion for a built release zip.

    python bin/verify-artifact.py dist/donjon-platform-7.5.0.zip

Exits non-zero (and prints the offending entries) if the artifact contains any
real DonjonSec lab host, the factory DB password, a private key, or runtime
state (data/results/.db/.git/.claude/tests/tools). Used by CI as the
post-build gate and runnable by hand. No network, stdlib + release_lib only.

Example/documentation CIDRs (192.168.1.0/24) and RFC1918 range constants
(192.168.0.0/16, 172.16.0.0/12) are NOT leaks and are permitted.
"""
from __future__ import annotations

import glob
import importlib.util
import sys
import zipfile
from pathlib import Path

REPO_ROOT = Path(__file__).resolve().parent.parent


def _load_release_lib():
    spec = importlib.util.spec_from_file_location(
        "release_lib", REPO_ROOT / "bin" / "release_lib.py"
    )
    mod = importlib.util.module_from_spec(spec)
    sys.modules["release_lib"] = mod
    spec.loader.exec_module(mod)
    return mod


# Real lab hosts that must NEVER appear (host octets, not the example CIDR).
_REAL_HOSTS = (
    b"192.0.2.100", b"192.0.2.110", b"192.0.2.116", b"192.0.2.118",
    b"192.0.2.119", b"192.0.2.125", b"192.0.2.101",
    b"192.0.2.1:", b"192.0.2.10", b"192.0.2.11",
    b"192.0.2.20", b"192.0.2.50",
)
_FACTORY_PW = (b"Factory" + b"Blue" + b"Flamingo" + b"2026")
# Assembled at runtime so this source file does not itself contain the literal
# key headers (which would trip the build's own secret gate when bin/ ships).
_PEM_HEADER = b"BEGIN " + b"PRIVATE KEY"
_OPENSSH_HEADER = b"BEGIN " + b"OPENSSH PRIVATE KEY"

_FORBIDDEN_PATH_MARKERS = (
    "/.git/", "/.claude", "/tests/", "/tools/",
    "__pycache__", "trial-license.json", ".trial-used",
    "eula_accepted.json",
)


def verify(zip_path: Path) -> int:
    rel = _load_release_lib()
    if not zip_path.exists():
        print(f"verify-artifact: ERROR file not found: {zip_path}", file=sys.stderr)
        return 2

    failures = []
    with zipfile.ZipFile(zip_path) as z:
        names = z.namelist()
        # 1. path-level forbidden trees / files
        for n in names:
            for marker in _FORBIDDEN_PATH_MARKERS:
                if marker in n:
                    failures.append(f"forbidden path: {n} (matched {marker!r})")
            if n.endswith((".db", ".pem", ".key")):
                failures.append(f"forbidden file type: {n}")
            if n.startswith(zip_path.stem.replace('.zip', '') + "/results/") or "/results/" in n:
                failures.append(f"runtime results entry: {n}")

        # 2. content-level real-host / secret leaks
        blob = b"".join(z.read(n) for n in names if not n.endswith("/"))
        for host in _REAL_HOSTS:
            c = blob.count(host)
            if c:
                failures.append(f"real lab host leaked: {host.decode()} x{c}")
        if _FACTORY_PW in blob:
            failures.append("factory DB password leaked")
        if _PEM_HEADER in blob or _OPENSSH_HEADER in blob:
            failures.append("private key material leaked")

        # 3. backstop: release_lib blocking gate per-entry
        for n in names:
            if n.endswith("/"):
                continue
            for f in rel.scan_blocking_leaks(z.read(n), n.split("/", 1)[-1]):
                failures.append(f"{n}: {f}")

    if failures:
        print(f"verify-artifact: FAIL ({len(failures)} issue(s)) for {zip_path.name}",
              file=sys.stderr)
        for f in sorted(set(failures))[:100]:
            print(f"  - {f}", file=sys.stderr)
        return 1

    print(f"verify-artifact: PASS  {zip_path.name} is sanitized "
          f"({len(names)} entries, no real-lab IP/secret/runtime-state)")
    return 0


def main(argv=None) -> int:
    argv = argv if argv is not None else sys.argv[1:]
    if not argv:
        print("usage: verify-artifact.py <artifact.zip> [more.zip ...]", file=sys.stderr)
        return 2
    # expand globs (CI passes dist/donjon-platform-*.zip)
    paths = []
    for a in argv:
        matched = glob.glob(a)
        paths.extend(matched if matched else [a])
    rc = 0
    for p in paths:
        rc |= verify(Path(p))
    return rc


if __name__ == "__main__":
    raise SystemExit(main())
