#!/usr/bin/env python3
"""Standalone verifier for the IHVRK Sovereign Evolution 7 evidence bundle.

Python standard library only. No network. Run it from a fresh directory that contains nothing but the
extracted bundle:

    python3 -I verify_evolution_07.py [--dir .] [--max-age-days N] [--source REPO] [--require-signature]
                                      [--trusted-key BASE64_ED25519_PUBLIC_KEY] [--json OUT]

What a PASS here means, and does not mean. This program checks that the bundle is complete, unmodified,
internally consistent, current, free of obvious secrets, and (with --source) that the recorded source hashes
match a checkout you supply. It reports, separately, what the bundle RECORDS about tests, formal models,
hosted CI, live endpoints, deployment, traffic path and independent review. It cannot re-run the tests (the
implementation source is not in the public bundle), so a recorded PASS is the developer's assertion with
transcripts attached, not something this program observed. Integrity is not truth: a valid hash shows a file
is the one that was listed, not that what it says is correct.

Exit codes (first failure wins, checked in this order):
   0  every required check passed
   2  usage error, or the bundle directory / manifest is missing
  10  malformed manifest or unsupported schema
  11  a required artifact is missing
  12  a file's SHA-256 or size differs from the manifest
  13  a file is present that the manifest does not list
  14  signature required or present, and it is missing, invalid, or cannot be checked
  15  required tests are missing, or recorded results are inconsistent with their own details
  16  evidence is stale (older than --max-age-days), from the future, or does not match --source
  17  contradictory statuses (for example a PASS gate whose evidence says FAIL / NOT_RUN / BLOCKED)
  18  secret or private material found in the bundle
  19  the bundle version is listed as withdrawn
"""
import argparse
import base64
import hashlib
import json
import os
import re
import sys
import time

SUPPORTED_MANIFEST = ("cain42.sovereign.evo07.manifest.v1",)
STATUSES = ("PASS", "FAIL", "PARTIAL", "BLOCKED", "NOT_RUN", "UNKNOWN")
REQUIRED_FILES = (
    "manifest.json", "baseline.json", "release_gate.json", "evidence_index.json", "test_results.json",
    "formal_results.json", "mutation_results.json", "claim_evidence_matrix.json", "cross_site_status.json",
    "enforcement_inventory.json", "environment.json", "classification.json",
    "BASELINE_RECONCILIATION.md", "THREAT_MODEL.md", "JIT_PROTOCOL_REVIEW.md", "ENFORCEMENT_COVERAGE.md",
    "WORKER_ISOLATION_REVIEW.md", "REPLAY_LEDGER_REVIEW.md", "FORMAL_VERIFICATION.md", "ADVERSARIAL_RESULTS.md",
    "IHVRK_REPRODUCTION.md", "RESIDUAL_RISKS.md", "RELEASE_GATE.md", "FINAL_REPORT.md", "TRUST_MODEL.md",
    "verify_evolution_07.py",
)
REQUIRED_GATES = tuple(f"G{n:02d}" for n in range(1, 15))
PRODUCTION_MANDATORY = ("G11", "G12", "G14")           # hosted CI, deployed revision + traffic path, independent review
REQUIRED_ATTACK_CLASSES = (
    "forged signatures", "altered signed fields", "duplicate JSON keys", "canonicalization mismatch",
    "cross-tenant token reuse", "identity spoofing", "target substitution", "parameter substitution",
    "scope widening", "stale policy digest", "replay races", "key revocation races", "clock rollback",
    "ledger corruption", "worker escape attempts", "inherited descriptor leakage", "symlink races",
    "shell metacharacters", "environment injection", "argument injection", "path traversal",
    "unauthorized network access", "DNS-based egress", "resource exhaustion", "output flooding",
    "process-tree survival", "timeout bypass", "cancellation races", "cache poisoning", "protocol downgrade",
    "corrupted evidence", "stale manifests", "verifier exceptions", "publication of secrets",
)
PRIVATE_SUFFIXES = (".py", ".pyc", ".pem", ".key", ".p12", ".pfx", ".db", ".sqlite", ".env", ".so", ".sh")
ALLOWED_PY = ("verify_evolution_07.py",)
SECRET_PATTERNS = (
    ("private key block", re.compile(rb"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
    ("AWS access key id", re.compile(rb"\bAKIA[0-9A-Z]{16}\b")),
    ("Stripe live key", re.compile(rb"\b[sr]k_live_[0-9a-zA-Z]{16,}")),
    ("GitHub token", re.compile(rb"\bgh[pousr]_[0-9A-Za-z]{30,}")),
    ("Slack token", re.compile(rb"\bxox[baprs]-[0-9A-Za-z-]{10,}")),
    ("Anthropic/OpenAI style key", re.compile(rb"\bsk-(?:ant-)?[A-Za-z0-9_-]{32,}")),
    ("JWT", re.compile(rb"\beyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}")),
    ("password assignment", re.compile(rb"(?i)\b(?:password|passwd|secret_key|api_key)['\"]?\s*[=:]\s*['\"][^'\"\s]{8,}['\"]")),
)


class Fail(Exception):
    def __init__(self, code, message):
        super().__init__(message)
        self.code, self.message = code, message


def sha256_file(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def load_json(path, code=10):
    def pairs(items):
        out = {}
        for k, v in items:
            if k in out:
                raise Fail(code, f"duplicate key {k!r} in {os.path.basename(path)}")
            out[k] = v
        return out
    try:
        with open(path, "rb") as f:
            return json.loads(f.read().decode("utf-8"), object_pairs_hook=pairs)
    except Fail:
        raise
    except (OSError, ValueError) as e:
        raise Fail(code, f"cannot parse {os.path.basename(path)}: {type(e).__name__}")


def manifest_signing_bytes(manifest):
    body = {k: v for k, v in manifest.items() if k != "signature"}
    return b"CAIN42/IHVRK-EVOLUTION-07-MANIFEST/v1\n" + json.dumps(body, sort_keys=True, separators=(",", ":")).encode()


def check_manifest(root):
    path = os.path.join(root, "manifest.json")
    if not os.path.isfile(path):
        raise Fail(2, "manifest.json not found in the bundle directory")
    m = load_json(path)
    if not isinstance(m, dict) or m.get("schema") not in SUPPORTED_MANIFEST:
        raise Fail(10, "unsupported or missing manifest schema")
    for key, typ in (("files", dict), ("run_id", str), ("generated_at_epoch", int), ("source_revision", str),
                     ("bundle_version", str), ("evolution", int), ("signature", dict), ("working_tree", dict),
                     ("source_tree_digest", str), ("withdrawn_versions", list)):
        if not isinstance(m.get(key), typ) or isinstance(m.get(key), bool):
            raise Fail(10, f"manifest field {key!r} missing or of the wrong type")
    if m["evolution"] != 7 or not re.fullmatch(r"[0-9a-f]{40}", m["source_revision"]):
        raise Fail(10, "manifest evolution or source revision malformed")
    for name, ent in m["files"].items():
        if (not isinstance(name, str) or name.startswith("/") or ".." in name.split("/") or "\\" in name
                or not isinstance(ent, dict) or not re.fullmatch(r"[0-9a-f]{64}", str(ent.get("sha256", "")))
                or not isinstance(ent.get("bytes"), int) or ent.get("classification") != "PUBLIC"
                or not isinstance(ent.get("artifact_id"), str)):
            raise Fail(10, f"malformed manifest entry {name!r}")
    ids = [e["artifact_id"] for e in m["files"].values()]
    if len(set(ids)) != len(ids):
        raise Fail(10, "duplicate artifact identifier in manifest")
    return m


def check_files(root, m):
    listed = set(m["files"]) | {"manifest.json"}
    for name in REQUIRED_FILES:
        if name not in listed or not os.path.isfile(os.path.join(root, name)):
            raise Fail(11, f"required artifact missing: {name}")
    for name, ent in sorted(m["files"].items()):
        p = os.path.join(root, name)
        if os.path.islink(p) or not os.path.isfile(p):
            raise Fail(11, f"listed artifact missing or not a regular file: {name}")
        if os.path.getsize(p) != ent["bytes"] or sha256_file(p) != ent["sha256"]:
            raise Fail(12, f"hash or size mismatch: {name}")
    present = set()
    for d, _dirs, files in os.walk(root):
        for f in files:
            present.add(os.path.relpath(os.path.join(d, f), root).replace(os.sep, "/"))
    extra = sorted(present - listed)
    if extra:
        raise Fail(13, f"file not listed in the manifest: {extra[0]}" + (f" (+{len(extra) - 1} more)" if len(extra) > 1 else ""))


def check_signature(m, require, trusted_key):
    sig = m["signature"]
    status = sig.get("status")
    if status == "UNSIGNED":
        if require:
            raise Fail(14, "signature required but the manifest is unsigned")
        return "NOT_RUN", "manifest is unsigned (integrity by SHA-256 only); see TRUST_MODEL.md"
    if status != "SIGNED" or sig.get("algorithm") != "Ed25519" or not isinstance(sig.get("value"), str) \
            or not isinstance(sig.get("kid"), str):
        raise Fail(14, "signature block malformed or algorithm not allowed")
    if not trusted_key:
        raise Fail(14, "manifest is signed but no --trusted-key was given; a key taken from the bundle proves nothing")
    try:
        from cryptography.exceptions import InvalidSignature
        from cryptography.hazmat.primitives.asymmetric import ed25519
    except Exception:
        raise Fail(14, "cannot check the signature: the 'cryptography' package is not installed")
    try:
        pub = ed25519.Ed25519PublicKey.from_public_bytes(base64.b64decode(trusted_key, validate=True))
        pub.verify(base64.b64decode(sig["value"], validate=True), manifest_signing_bytes(m))
    except (InvalidSignature, ValueError):
        raise Fail(14, "manifest signature does not verify under the trusted key")
    return "PASS", f"Ed25519 signature by {sig['kid']} verified under the key you supplied"


def check_results(root, m):
    tr = load_json(os.path.join(root, "test_results.json"), 15)
    suites = tr.get("suites")
    if not isinstance(suites, list) or not suites:
        raise Fail(15, "test_results.json has no suites")
    tot = {"tests": 0, "passed": 0, "failed": 0, "errors": 0, "skipped": 0}
    for s in suites:
        cases = s.get("cases")
        if not isinstance(cases, list) or s.get("exit_code") is None:
            raise Fail(15, f"suite {s.get('name')!r} has no case list or no exit code")
        c = {k: sum(1 for x in cases if x.get("outcome") == k) for k in ("passed", "failed", "error", "skipped")}
        if (c["passed"], c["failed"], c["error"], c["skipped"], len(cases)) != \
                (s.get("passed"), s.get("failed"), s.get("errors"), s.get("skipped"), s.get("tests")):
            raise Fail(15, f"suite {s.get('name')!r}: summary counts do not match its own case list")
        if (s["failed"] + s["errors"] == 0) != (s["exit_code"] == 0):
            raise Fail(15, f"suite {s.get('name')!r}: exit code contradicts its results")
        for k in tot:
            tot[k] += s[k]
    if tr.get("totals") != tot:
        raise Fail(15, "test_results.json totals do not equal the sum of its suites")
    labelled = [x for s in suites for x in s["cases"] if x.get("test_id")]
    classes = {x.get("attack_class") for x in labelled if x.get("outcome") == "passed"}
    missing = [c for c in REQUIRED_ATTACK_CLASSES if c not in classes]
    if missing:
        raise Fail(15, f"required attack class without a passing labelled test: {missing[0]}")
    fr = load_json(os.path.join(root, "formal_results.json"), 15)
    props = fr.get("properties")
    if not isinstance(props, list) or len(props) < 18:
        raise Fail(15, "formal_results.json lists fewer than 18 properties")
    holding = [p for p in props if p.get("holds") is True and p.get("control_sat") == "sat"
               and p.get("mutant_result") == p.get("mutant_expected")]
    if fr.get("summary", {}).get("holding") != len(holding) or fr["summary"].get("total") != len(props):
        raise Fail(15, "formal_results.json summary does not match its property list")
    mr = load_json(os.path.join(root, "mutation_results.json"), 15)
    muts = mr.get("mutants")
    if not isinstance(muts, list) or not muts:
        raise Fail(15, "mutation_results.json has no mutants")
    counts = {}
    for x in muts:
        counts[x.get("status")] = counts.get(x.get("status"), 0) + 1
        if x.get("status") == "SURVIVED_EQUIVALENT" and not x.get("equivalent_justification"):
            raise Fail(15, f"mutant {x.get('id')} is marked equivalent without a justification")
    if mr.get("counts") != counts or mr.get("total") != len(muts):
        raise Fail(15, "mutation_results.json counts do not match its mutant list")
    bad = sorted(x["id"] for x in muts if x.get("status") not in ("KILLED", "SURVIVED_EQUIVALENT"))
    if mr.get("unexplained_survivors_or_errors") != bad:
        raise Fail(15, "mutation_results.json survivor list does not match its mutant list")
    return {"tests": tot, "formal_total": len(props), "formal_holding": len(holding), "mutants": counts,
            "mutation_unexplained": bad, "labelled_cases": len(labelled),
            "suites_source_digest": tr.get("source_tree_digest"), "formal_digest": fr.get("source_tree_digest"),
            "mutation_digest": mr.get("source_tree_digest")}


def check_freshness(root, m, results, max_age_days, source):
    now = int(time.time())
    age = now - m["generated_at_epoch"]
    if age < -300:
        raise Fail(16, "bundle generation time is in the future")
    if max_age_days is not None and age > max_age_days * 86400:
        raise Fail(16, f"evidence is stale: generated {age // 86400} days ago (limit {max_age_days})")
    for name in ("suites_source_digest", "formal_digest", "mutation_digest"):
        if results[name] != m["source_tree_digest"]:
            raise Fail(16, f"stale evidence: {name} was produced from a different source tree than the manifest")
    if not source:
        return "NOT_RUN", "no --source checkout supplied; recorded source hashes were not compared with any code"
    base = load_json(os.path.join(root, "baseline.json"), 16)
    diffs = []
    for rel, digest in sorted(base.get("source_files", {}).items()):
        p = os.path.join(source, rel)
        if not os.path.isfile(p) or sha256_file(p) != digest:
            diffs.append(rel)
    if diffs:
        raise Fail(16, f"source checkout does not match the bundle: {diffs[0]}" + (f" (+{len(diffs) - 1} more)" if len(diffs) > 1 else ""))
    return "PASS", f"{len(base['source_files'])} source files in the supplied checkout match the recorded hashes"


def check_consistency(root, m, results):
    g = load_json(os.path.join(root, "release_gate.json"), 17)
    gates = g.get("gates")
    if not isinstance(gates, dict) or sorted(gates) != sorted(REQUIRED_GATES):
        raise Fail(17, "release_gate.json does not carry exactly the fourteen required gates")
    for gid, gate in gates.items():
        if gate.get("status") not in STATUSES or not isinstance(gate.get("evidence"), str) or not gate["evidence"]:
            raise Fail(17, f"gate {gid} has an invalid status or no evidence")
    if g.get("run_id") != m["run_id"] or g.get("source_revision") != m["source_revision"]:
        raise Fail(17, "release gate belongs to a different run or revision than the manifest")
    t = results["tests"]
    clean = t["failed"] == 0 and t["errors"] == 0
    for gid in ("G01", "G02", "G03", "G04", "G05", "G07"):
        if gates[gid]["status"] == "PASS" and not clean:
            raise Fail(17, f"gate {gid} is PASS while recorded tests include failures or errors")
    if gates["G06"]["status"] == "PASS" and results["formal_holding"] != results["formal_total"]:
        raise Fail(17, "gate G06 is PASS while a formal property does not hold")
    if gates["G07"]["status"] == "PASS" and results["mutation_unexplained"]:
        raise Fail(17, "gate G07 is PASS while a mutant survived without an explanation")
    cs = load_json(os.path.join(root, "cross_site_status.json"), 17)
    if gates["G10"]["status"] == "PASS" and (cs.get("evolution_07_published") is not True or cs.get("parity") != "PASS"):
        raise Fail(17, "gate G10 is PASS but the cross-site record does not show Evolution 7 published with parity")
    base = load_json(os.path.join(root, "baseline.json"), 17)
    if gates["G11"]["status"] == "PASS" and not base.get("git", {}).get("remotes"):
        raise Fail(17, "gate G11 (hosted CI) is PASS but the baseline records no git remote")
    if gates["G14"]["status"] == "PASS" and not g.get("independent_review", {}).get("reviewer"):
        raise Fail(17, "gate G14 (independent review) is PASS without a recorded reviewer")
    required_local = [gid for gid, gate in gates.items() if gate.get("scope") == "local"]
    local_ok = all(gates[gid]["status"] == "PASS" for gid in required_local)
    want_local = "PASS" if local_ok else ("FAIL" if any(gates[x]["status"] == "FAIL" for x in required_local) else "PARTIAL")
    if g.get("local_development_gate") != want_local:
        raise Fail(17, f"local_development_gate says {g.get('local_development_gate')} but its gates give {want_local}")
    prod_ok = all(gate["status"] == "PASS" for gid, gate in gates.items() if gate.get("mandatory_for_production"))
    for gid in PRODUCTION_MANDATORY:
        if gates[gid].get("mandatory_for_production") is not True:
            raise Fail(17, f"gate {gid} must be mandatory for production")
    if (g.get("production_release_gate") == "PASS") != prod_ok or g.get("production_release_gate") not in ("PASS", "BLOCKED", "FAIL"):
        raise Fail(17, "production_release_gate contradicts its mandatory gates")
    matrix = load_json(os.path.join(root, "claim_evidence_matrix.json"), 17)
    for claim in matrix.get("claims", []):
        if claim.get("status") not in STATUSES:
            raise Fail(17, f"claim {claim.get('id')} has an invalid status")
        for art in claim.get("artifacts", []):
            if art not in m["files"]:
                raise Fail(17, f"claim {claim.get('id')} cites an artifact that is not in the bundle: {art}")
    return g, gates


def check_secrets(root, m):
    for name in sorted(m["files"]) + ["manifest.json"]:
        low = name.lower()
        if low.endswith(PRIVATE_SUFFIXES) and os.path.basename(name) not in ALLOWED_PY:
            raise Fail(18, f"private file type in a public bundle: {name}")
        with open(os.path.join(root, name), "rb") as f:
            data = f.read()
        if os.path.basename(name) in ALLOWED_PY:
            continue                                  # this program contains the patterns it searches for
        for label, pat in SECRET_PATTERNS:
            if pat.search(data):
                raise Fail(18, f"{label} found in {name}")


def main(argv=None):
    ap = argparse.ArgumentParser(description="Verify the IHVRK Sovereign Evolution 7 evidence bundle")
    ap.add_argument("--dir", default=os.path.dirname(os.path.abspath(__file__)))
    ap.add_argument("--max-age-days", type=float, default=30.0)
    ap.add_argument("--allow-stale", action="store_true")
    ap.add_argument("--source")
    ap.add_argument("--require-signature", action="store_true")
    ap.add_argument("--trusted-key")
    ap.add_argument("--json")
    try:
        args = ap.parse_args(argv)
    except SystemExit:
        return 2
    root = os.path.abspath(args.dir)
    report = {"verifier": "verify_evolution_07.py", "bundle_dir": root, "checks": {}, "recorded": {}}
    code = 0
    try:
        if not os.path.isdir(root):
            raise Fail(2, "bundle directory not found")
        m = check_manifest(root)
        report["bundle_version"], report["run_id"], report["source_revision"] = m["bundle_version"], m["run_id"], m["source_revision"]
        report["manifest_sha256"] = sha256_file(os.path.join(root, "manifest.json"))
        check_files(root, m)
        report["checks"]["artifact_integrity"] = ["PASS", f"{len(m['files'])} listed artifacts match SHA-256 and size; no unlisted file"]
        report["checks"]["signature"] = list(check_signature(m, args.require_signature, args.trusted_key))
        results = check_results(root, m)
        report["checks"]["results_internal_consistency"] = ["PASS", "summaries equal their own detail records"]
        report["checks"]["source_inspection"] = list(check_freshness(
            root, m, results, None if args.allow_stale else args.max_age_days, args.source))
        report["checks"]["freshness"] = ["PASS" if not args.allow_stale else "NOT_RUN",
                                         f"generated {max(0, int(time.time()) - m['generated_at_epoch']) // 3600} h ago"]
        g, gates = check_consistency(root, m, results)
        report["checks"]["status_consistency"] = ["PASS", "no gate or claim contradicts its evidence"]
        check_secrets(root, m)
        report["checks"]["secret_and_private_scan"] = ["PASS", "no secret pattern and no private file type"]
        if m["bundle_version"] in m["withdrawn_versions"]:
            raise Fail(19, f"bundle version {m['bundle_version']} is withdrawn")
        report["checks"]["withdrawn_version"] = ["PASS", "version is not in the withdrawn list"]
        report["recorded"] = {
            "local_tests": f"{results['tests']['passed']} passed, {results['tests']['failed']} failed, "
                           f"{results['tests']['errors']} errors, {results['tests']['skipped']} skipped",
            "formal_models": f"{results['formal_holding']}/{results['formal_total']} properties hold in their models",
            "mutation": json.dumps(results["mutants"], sort_keys=True),
            "gates": {gid: gates[gid]["status"] for gid in sorted(gates)},
            "local_development_gate": g["local_development_gate"],
            "production_release_gate": g["production_release_gate"],
            "working_tree_clean": m["working_tree"].get("clean"),
        }
    except Fail as e:
        code = e.code
        report["failure"] = {"exit_code": e.code, "message": e.message}
    report["exit_code"] = code
    if args.json:
        with open(args.json, "w") as f:
            json.dump(report, f, indent=2, sort_keys=True)
    print("IHVRK Sovereign Evolution 7 bundle verifier")
    for k in ("bundle_version", "run_id", "source_revision", "manifest_sha256"):
        if k in report:
            print(f"  {k}: {report[k]}")
    print("VERIFIED HERE (this program checked these itself):")
    for name, (status, detail) in report["checks"].items():
        print(f"  [{status:7s}] {name}: {detail}")
    if code == 0:
        print("RECORDED BY THE DEVELOPER (read from the bundle; NOT re-run or observed by this program):")
        for k, v in report["recorded"].items():
            if k != "gates":
                print(f"  {k}: {v}")
        print("  gates: " + " ".join(f"{k}={v}" for k, v in report["recorded"]["gates"].items()))
        print("RESULT: PASS -- the bundle is intact and self-consistent. This is not a statement that the system is "
              "secure, deployed, or independently reviewed.")
    else:
        print(f"RESULT: FAIL (exit {code}) -- {report['failure']['message']}")
    return code


if __name__ == "__main__":
    sys.exit(main())
