#!/usr/bin/env python3 """Assert every ssh-certificate resource declares its ceilings in the manifest. Read-only. Reads `inventory.yaml` and the flex-auth registry snapshot; touches no network, no OpenBao, and no secret material. Why this exists (FLEX-DEC-2026-012, 2026-09-07). flex-auth's enrichment used to overlay registry facts additive-if-absent, so a caller-supplied value for a key won and the registry's ceiling never applied. That is fixed on their side: registry facts now win. But they win only where the registry HAS a value — "A resource whose manifest omits max_ttl_hours hands that ceiling back to the caller, and registering the resource is not sufficient; the specific key must be present." So there are two ways to hand a ceiling back, and this check covers both: 1. An actor with no manifest resource at all. `warden sign` names `ssh-cert:actor/` for any actor in inventory, whether or not the snapshot was regenerated. Adding an actor and forgetting to rebuild is an honour-system step in `SCOPE.md`, and honour-system steps are what `ADR-0004`'s WARDEN_AGENT_ID marker taught us to stop relying on. 2. A manifest resource missing one of the ceiling or allowlist keys. The builder emits all of them today; this asserts it stays true, including for resources added by hand or by a future code path. ops-warden sends no `resource.attributes` on a CheckRequest (`src/warden/policy.py`), which is why FLEX-DEC-2026-012 was defence-in-depth rather than a live hole for this repo. That property is asserted separately in `tests/test_policy.py`; this check covers the half that survives it — if anything ever does reach a request field, the ceiling it would have to beat must actually exist. Usage: python scripts/check_flex_auth_manifest_coverage.py \ --inventory examples/inventory.seed.yaml \ --registry registry/flex-auth/production_registry_snapshot.json [--json] Exit: 0 covered, 2 coverage gap, 1 usage/IO error. """ from __future__ import annotations import argparse import json import sys from pathlib import Path from typing import Any import yaml #: Every key ops-warden's shipped policy package branches on as a ceiling or an #: allowlist. Named by flex-auth in FLEX-DEC-2026-012; kept here rather than #: derived from the package so a package edit that drops a branch does not #: silently shrink what this check requires. REQUIRED_CEILING_KEYS = ( "actor_id", "actor_type", "allowed_principals", "allowed_subjects", "max_ttl_hours", "security_zone", "security_zone_admission", ) RESOURCE_TYPE = "ssh-certificate" def _resources(registry: dict[str, Any]) -> list[dict[str, Any]]: out: list[dict[str, Any]] = [] for manifest in registry.get("resource_manifests") or []: out.extend(manifest.get("resources") or []) return out def audit(inventory: dict[str, Any], registry: dict[str, Any]) -> dict[str, Any]: actors = sorted((inventory.get("actors") or {}).keys()) resources = _resources(registry) by_id = {str(r.get("id")): r for r in resources} unregistered: list[str] = [] missing_keys: list[dict[str, Any]] = [] for name in actors: if f"ssh-cert:actor/{name}" not in by_id: unregistered.append(name) # Check every ssh-certificate resource in the manifest, not only the ones an # inventory actor maps to: a resource flex-auth can be asked about is one it # holds, regardless of where it came from. for resource in resources: if str(resource.get("type")) != RESOURCE_TYPE: continue attributes = resource.get("attributes") or {} absent = [ key for key in REQUIRED_CEILING_KEYS # A null is not a declaration: flex-auth overlays a registry value # only where one exists, so `key: null` hands the ceiling back # exactly as an absent key does. if attributes.get(key) is None ] if absent: missing_keys.append({"resource": str(resource.get("id")), "missing": absent}) # Not a security defect — a stale resource has ceilings, it just has no # actor. Reported so drift is visible rather than accumulating silently. orphaned = sorted( rid for rid, r in by_id.items() if str(r.get("type")) == RESOURCE_TYPE and rid.removeprefix("ssh-cert:actor/") not in actors ) return { "actors": len(actors), "resources": sum(1 for r in resources if str(r.get("type")) == RESOURCE_TYPE), "required_keys": list(REQUIRED_CEILING_KEYS), "unregistered_actors": unregistered, "resources_missing_keys": missing_keys, "orphaned_resources": orphaned, "covered": not unregistered and not missing_keys, } def main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--inventory", default="examples/inventory.seed.yaml", type=Path) parser.add_argument( "--registry", default="registry/flex-auth/production_registry_snapshot.json", type=Path, ) parser.add_argument("--json", action="store_true", dest="as_json") args = parser.parse_args(argv) try: inventory = yaml.safe_load(args.inventory.read_text()) or {} registry = json.loads(args.registry.read_text()) except (OSError, ValueError) as exc: print(f"error: {exc}", file=sys.stderr) return 1 report = audit(inventory, registry) report["inventory"] = str(args.inventory) report["registry"] = str(args.registry) if args.as_json: print(json.dumps(report, indent=2)) else: print(f"inventory : {args.inventory} ({report['actors']} actors)") print(f"registry : {args.registry} ({report['resources']} ssh-certificate resources)") for name in report["unregistered_actors"]: print(f" MISSING {name} — no manifest resource; every ceiling is caller-supplied") for row in report["resources_missing_keys"]: print(f" UNDECLARED {row['resource']} — {', '.join(row['missing'])}") for rid in report["orphaned_resources"]: print(f" orphaned {rid} — manifest resource with no inventory actor") print("covered" if report["covered"] else "NOT COVERED") return 0 if report["covered"] else 2 if __name__ == "__main__": raise SystemExit(main())