teleo-infrastructure/ops/verify_vps_canonical_snapshot_delta.py

284 lines
12 KiB
Python

#!/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 write_private_json
from .verify_postgres_parity_manifest import 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 write_private_json
from verify_postgres_parity_manifest import 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 ""))),
"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_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),
**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),
**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(json.dumps(payload, indent=2, sort_keys=True))
return 0 if payload.get("status") == "pass" else 1
if __name__ == "__main__":
raise SystemExit(main())