#!/usr/bin/env python3 """Bind a postflight VPS manifest to a captured snapshot and report exact deltas.""" from __future__ import annotations import argparse import json import re import stat from datetime import UTC, datetime from pathlib import Path from typing import Any try: from .capture_vps_canonical_postgres_snapshot import ( SOURCE_SNAPSHOT_RECEIPT_SCHEMA, build_provenance_binding, ) from .private_receipt_io import print_private_receipt_summary, write_private_json from .verify_postgres_parity_manifest import ( REVIEWED_MANIFEST_SQL_SHA256, SINGLETON_KINDS, canonical_hash, file_sha256, load_manifest, ) except ImportError: # pragma: no cover - direct script execution from capture_vps_canonical_postgres_snapshot import SOURCE_SNAPSHOT_RECEIPT_SCHEMA, build_provenance_binding from private_receipt_io import print_private_receipt_summary, write_private_json from verify_postgres_parity_manifest import ( REVIEWED_MANIFEST_SQL_SHA256, SINGLETON_KINDS, canonical_hash, file_sha256, load_manifest, ) DELTA_SCHEMA = "livingip.vpsCanonicalSnapshotDelta.v1" SHA256_RE = re.compile(r"[0-9a-f]{64}\Z") def service_healthy(value: Any) -> bool: if not isinstance(value, dict): return False try: main_pid = int(value.get("MainPID") or 0) restarts = int(value.get("NRestarts") or 0) except (TypeError, ValueError): return False return ( value.get("ActiveState") == "active" and value.get("SubState") == "running" and main_pid > 0 and restarts >= 0 ) def regular_file(path: Path, label: str) -> Path: try: mode = path.lstat().st_mode except OSError as exc: raise ValueError(f"{label} is unavailable: {exc}") from exc if not stat.S_ISREG(mode) or path.is_symlink(): raise ValueError(f"{label} must be a regular non-symlink file") return path.resolve() def load_json(path: Path, label: str) -> dict[str, Any]: path = regular_file(path, label) try: payload = json.loads(path.read_text(encoding="utf-8")) except json.JSONDecodeError as exc: raise ValueError(f"{label} is invalid JSON") from exc if not isinstance(payload, dict): raise ValueError(f"{label} must be a JSON object") return payload def parse_timestamp(value: Any, label: str) -> datetime: text = str(value or "") if text.endswith("Z"): text = text[:-1] + "+00:00" try: parsed = datetime.fromisoformat(text) except ValueError as exc: raise ValueError(f"{label} is not an ISO-8601 timestamp") from exc if parsed.tzinfo is None: raise ValueError(f"{label} must include a timezone") return parsed.astimezone(UTC) def validate_capture( receipt_path: Path, manifest_path: Path, *, expected_authorization_ref: str, label: str, ) -> tuple[dict[str, Any], dict[str, Any]]: receipt = load_json(receipt_path, f"{label} receipt") manifest_path = regular_file(manifest_path, f"{label} manifest") manifest = load_manifest(manifest_path) snapshot = receipt.get("snapshot") or {} source = receipt.get("source") or {} dump = receipt.get("dump") or {} manifest_receipt = receipt.get("manifest") or {} source_context = receipt.get("source_context") or {} source_service = receipt.get("source_service") or {} binding = receipt.get("provenance_binding") or {} expected_binding = build_provenance_binding( run_id=str(receipt.get("run_id") or ""), authorization_ref=str(receipt.get("capture_authorization_ref") or ""), source=source, snapshot=snapshot, dump_sha256=str(dump.get("sha256") or ""), manifest_sql_sha256=str(manifest_receipt.get("manifest_sql_sha256") or ""), source_manifest_sha256=file_sha256(manifest_path), source_context_sha256=str(source_context.get("sha256") or ""), ) total_rows = sum(int(row["row_count"]) for row in manifest["tables"].values()) checks = { "schema": receipt.get("schema") == SOURCE_SNAPSHOT_RECEIPT_SCHEMA, "receipt_version": receipt.get("receipt_version") == 2, "status": receipt.get("status") == "pass", "authorization": receipt.get("capture_authorization_ref") == expected_authorization_ref, "snapshot_exported": receipt.get("snapshot_exported") is True, "service_unchanged": receipt.get("service_unchanged") is True, "source_service_healthy": source_service.get("unchanged") is True and service_healthy(source_service.get("before")) and source_service.get("before") == source_service.get("after"), "source_not_mutated": receipt.get("production_db_mutated") is False, "telegram_not_sent": receipt.get("telegram_send_attempted") is False, "manifest_hash": manifest_receipt.get("sha256") == file_sha256(manifest_path), "manifest_table_count": manifest_receipt.get("table_count") == len(manifest["tables"]), "manifest_total_rows": manifest_receipt.get("total_rows") == total_rows, "manifest_read_only": manifest["singleton"]["identity"].get("transaction_read_only") == "on", "manifest_database": manifest["singleton"]["identity"].get("database") == source.get("database"), "binding_payload": binding.get("payload") == expected_binding["payload"], "binding_sha256": binding.get("sha256") == expected_binding["sha256"], "binding_algorithm": binding.get("algorithm") == "sha256", "dump_sha256": bool(SHA256_RE.fullmatch(str(dump.get("sha256") or ""))), "manifest_sql_sha256": bool(SHA256_RE.fullmatch(str(manifest_receipt.get("manifest_sql_sha256") or ""))), "manifest_sql_reviewed": manifest_receipt.get("manifest_sql_sha256") == REVIEWED_MANIFEST_SQL_SHA256, "source_context_sha256": bool(SHA256_RE.fullmatch(str(source_context.get("sha256") or ""))), "snapshot_identity": all( bool(snapshot.get(field)) for field in ("captured_at_utc", "exported_snapshot_id", "txid_snapshot", "wal_lsn", "system_identifier") ), } failed = sorted(name for name, passed in checks.items() if not passed) if failed: raise ValueError(f"{label} capture failed validation: " + ", ".join(failed)) return receipt, manifest def manifest_fingerprint(manifest: dict[str, Any]) -> dict[str, Any]: tables = { name: {"row_count": int(row["row_count"]), "rowset_md5": row.get("rowset_md5")} for name, row in sorted(manifest["tables"].items()) } structures = { kind: canonical_hash(manifest["singleton"][kind].get("items", [])) for kind in sorted(SINGLETON_KINDS - {"identity"}) } return { "table_count": len(tables), "total_rows": sum(row["row_count"] for row in tables.values()), "table_fingerprint_sha256": canonical_hash(tables), "structure_fingerprint_sha256": canonical_hash(structures), "tables": tables, "structures": structures, } def verify_delta( *, baseline_receipt_path: Path, baseline_manifest_path: Path, baseline_authorization_ref: str, current_receipt_path: Path, current_manifest_path: Path, current_authorization_ref: str, ) -> dict[str, Any]: baseline_receipt, baseline_manifest = validate_capture( baseline_receipt_path, baseline_manifest_path, expected_authorization_ref=baseline_authorization_ref, label="baseline", ) current_receipt, current_manifest = validate_capture( current_receipt_path, current_manifest_path, expected_authorization_ref=current_authorization_ref, label="current", ) baseline_snapshot = baseline_receipt["snapshot"] current_snapshot = current_receipt["snapshot"] baseline_time = parse_timestamp(baseline_snapshot["captured_at_utc"], "baseline snapshot time") current_time = parse_timestamp(current_snapshot["captured_at_utc"], "current snapshot time") if current_time <= baseline_time: raise ValueError("current source capture must be newer than the restored baseline capture") if current_receipt.get("source") != baseline_receipt.get("source"): raise ValueError("current source identity does not match the restored baseline source") if current_snapshot.get("system_identifier") != baseline_snapshot.get("system_identifier"): raise ValueError("current source PostgreSQL system identifier changed") baseline_manifest_sql_sha256 = str((baseline_receipt.get("manifest") or {}).get("manifest_sql_sha256") or "") current_manifest_sql_sha256 = str((current_receipt.get("manifest") or {}).get("manifest_sql_sha256") or "") if baseline_manifest_sql_sha256 != current_manifest_sql_sha256: raise ValueError("baseline and current captures used different manifest SQL algorithms") baseline = manifest_fingerprint(baseline_manifest) current = manifest_fingerprint(current_manifest) baseline_tables = baseline.pop("tables") current_tables = current.pop("tables") baseline_structures = baseline.pop("structures") current_structures = current.pop("structures") added_tables = sorted(set(current_tables) - set(baseline_tables)) removed_tables = sorted(set(baseline_tables) - set(current_tables)) changed_tables = [ {"table": table, "baseline": baseline_tables[table], "current": current_tables[table]} for table in sorted(set(baseline_tables) & set(current_tables)) if baseline_tables[table] != current_tables[table] ] changed_structures = [ { "kind": kind, "baseline_sha256": baseline_structures.get(kind), "current_sha256": current_structures.get(kind), } for kind in sorted(set(baseline_structures) | set(current_structures)) if baseline_structures.get(kind) != current_structures.get(kind) ] delta_detected = bool(added_tables or removed_tables or changed_tables or changed_structures) return { "artifact": "vps_canonical_snapshot_postflight_delta", "schema": DELTA_SCHEMA, "generated_at_utc": datetime.now(UTC).isoformat(), "status": "pass", "baseline": { "run_id": baseline_receipt["run_id"], "capture_authorization_ref": baseline_authorization_ref, "captured_at_utc": baseline_snapshot["captured_at_utc"], "capture_receipt_sha256": file_sha256(baseline_receipt_path), "manifest_sha256": file_sha256(baseline_manifest_path), "manifest_sql_sha256": baseline_manifest_sql_sha256, **baseline, }, "current": { "run_id": current_receipt["run_id"], "capture_authorization_ref": current_authorization_ref, "captured_at_utc": current_snapshot["captured_at_utc"], "capture_receipt_sha256": file_sha256(current_receipt_path), "manifest_sha256": file_sha256(current_manifest_path), "manifest_sql_sha256": current_manifest_sql_sha256, **current, }, "delta": { "delta_detected": delta_detected, "added_tables": added_tables, "removed_tables": removed_tables, "changed_tables": changed_tables, "changed_structures": changed_structures, "total_row_delta": current["total_rows"] - baseline["total_rows"], }, "strongest_claim_allowed": ( "restored snapshot still matches the newest postflight VPS capture" if not delta_detected else "restored snapshot parity is bounded to its capture; post-snapshot VPS deltas are explicit" ), } def main() -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--baseline-receipt", required=True, type=Path) parser.add_argument("--baseline-manifest", required=True, type=Path) parser.add_argument("--baseline-authorization-ref", required=True) parser.add_argument("--current-receipt", required=True, type=Path) parser.add_argument("--current-manifest", required=True, type=Path) parser.add_argument("--current-authorization-ref", required=True) parser.add_argument("--output", required=True, type=Path) args = parser.parse_args() try: payload = verify_delta( baseline_receipt_path=args.baseline_receipt, baseline_manifest_path=args.baseline_manifest, baseline_authorization_ref=args.baseline_authorization_ref, current_receipt_path=args.current_receipt, current_manifest_path=args.current_manifest, current_authorization_ref=args.current_authorization_ref, ) except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError) as exc: payload = { "artifact": "vps_canonical_snapshot_postflight_delta", "schema": DELTA_SCHEMA, "generated_at_utc": datetime.now(UTC).isoformat(), "status": "fail", "error": str(exc), "strongest_claim_allowed": "post-snapshot VPS delta is not proven", } write_private_json(args.output, payload) print_private_receipt_summary(args.output, payload) return 0 if payload.get("status") == "pass" else 1 if __name__ == "__main__": raise SystemExit(main())