#!/usr/bin/env python3 """Fail closed unless a built policy site is safe to publish as an OCI release.""" from __future__ import annotations import argparse import hashlib import html import json from pathlib import Path, PurePosixPath import re from typing import Any HEX_DIGEST = re.compile(r"^[a-f0-9]{64}$") CLEAN_GIT_REVISION = re.compile(r"^[a-f0-9]{40}$") SOURCE_REVISION = re.compile( r'' ) SOURCE_DIGEST = re.compile(r'') def _safe_relative(value: str) -> PurePosixPath: path = PurePosixPath(value) if path.is_absolute() or ".." in path.parts or not path.parts: raise ValueError(f"unsafe release path: {value!r}") return path def _page_provenance(path: Path) -> tuple[str, str]: page = path.read_text(encoding="utf-8") revision = SOURCE_REVISION.search(page) digest = SOURCE_DIGEST.search(page) if not revision or not digest: raise ValueError(f"{path}: missing source provenance metadata") return html.unescape(revision.group(1)), html.unescape(digest.group(1)) def verify(build: Path) -> dict[str, Any]: build = build.resolve() if not build.is_dir(): raise ValueError(f"release directory does not exist: {build}") for path in build.rglob("*"): if path.is_symlink(): raise ValueError(f"release tree contains a symlink: {path.relative_to(build)}") index = build / "index.html" manifest_path = build / "publication-manifest.json" inventory_path = build / "source-inventory.json" if not index.is_file() or not manifest_path.is_file() or not inventory_path.is_file(): raise ValueError( "release requires index.html, publication-manifest.json and source-inventory.json" ) manifest_bytes = manifest_path.read_bytes() manifest = json.loads(manifest_bytes) if manifest.get("schema_version") != 1: raise ValueError("publication manifest schema_version must be 1") if not manifest.get("generated_as_of"): raise ValueError("publication manifest generated_as_of is required") documents = manifest.get("documents") if not isinstance(documents, list) or not documents: raise ValueError("publication manifest must contain at least one document") inventory_bytes = inventory_path.read_bytes() inventory = json.loads(inventory_bytes) if inventory.get("schema_version") != "policy-nexus-source-inventory/v1": raise ValueError("source inventory schema_version is invalid") source_set_digest = inventory.get("source_set_digest", "") if not HEX_DIGEST.fullmatch(source_set_digest): raise ValueError("source inventory requires a valid source_set_digest") inventoried_published = { (source.get("source_repo"), source.get("source_path")) for source in inventory.get("sources", []) if source.get("disposition") == "published" } verified: list[str] = [] for document in documents: document_id = document.get("id", "") for field in ( "id", "title", "status", "revision", "owner", "last_reviewed", "review_due", "canonical_path", "revision_path", "source_repo", "source_path", ): if not document.get(field) or document.get(field) == "unknown": raise ValueError(f"{document_id}: release metadata field {field} is required") source_revision = document.get("source_revision", "") source_digest = document.get("source_digest", "") if not CLEAN_GIT_REVISION.fullmatch(source_revision): raise ValueError( f"{document_id}: production source_revision must be a clean 40-hex Git commit; " f"got {source_revision!r}" ) if not HEX_DIGEST.fullmatch(source_digest): raise ValueError(f"{document_id}: invalid source_digest {source_digest!r}") source_key = (document["source_repo"], document["source_path"]) if source_key not in inventoried_published: raise ValueError(f"{document_id}: source is not published in source inventory") canonical = build / _safe_relative(document["canonical_path"]) revision = build / _safe_relative(document["revision_path"]) if not canonical.is_file() or not revision.is_file(): raise ValueError(f"{document_id}: canonical or immutable revision page is missing") current_revision, current_digest = _page_provenance(canonical) immutable_revision, immutable_digest = _page_provenance(revision) if (current_revision, current_digest) != (source_revision, source_digest): raise ValueError(f"{document_id}: canonical page provenance differs from manifest") if immutable_digest != source_digest: raise ValueError(f"{document_id}: immutable revision digest differs from manifest") if not CLEAN_GIT_REVISION.fullmatch(immutable_revision): raise ValueError( f"{document_id}: immutable revision page was not built from a clean Git commit" ) verified.append(document_id) return { "schema_version": "policy-nexus-release/v1", "publication_manifest_digest": hashlib.sha256(manifest_bytes).hexdigest(), "source_inventory_digest": hashlib.sha256(inventory_bytes).hexdigest(), "source_set_digest": source_set_digest, "generated_as_of": manifest["generated_as_of"], "documents": verified, } def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("build", nargs="?", type=Path, default=Path("build")) args = parser.parse_args() try: evidence = verify(args.build) except (KeyError, OSError, ValueError, json.JSONDecodeError) as exc: parser.error(str(exc)) print(json.dumps(evidence, sort_keys=True)) return 0 if __name__ == "__main__": raise SystemExit(main())