130 lines
5.1 KiB
Python
130 lines
5.1 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
"""Measure the classification coverage published in `pep-stance.yaml`.
|
||
|
|
|
||
|
|
Read-only. No network, no OpenBao, no secret material.
|
||
|
|
|
||
|
|
Why this exists. security-layer-model v0.8 §6.4 obligation 3 requires a dated
|
||
|
|
classification-coverage figure beside each stance, and §13.1 carries a Coverage
|
||
|
|
column whose first entries are ops-warden's — because ops-warden asked for the
|
||
|
|
column after measuring that 0 of 3 signing targets resolve to a zone.
|
||
|
|
|
||
|
|
Having asked for it, we own the figure's accuracy. A hand-counted number in a
|
||
|
|
published register decays silently: the register explicitly does not compute
|
||
|
|
anyone's coverage, and a stale figure beside a marked cell is worse than a blank,
|
||
|
|
which at least reads as "not reported". So the figure is measured from the same
|
||
|
|
two artifacts the runtime uses, and `tests/test_layer_conformance.py` asserts the
|
||
|
|
published block equals what this reports — the same property that makes the stance
|
||
|
|
map worth publishing (`pep-stance.yaml` equals `PolicyConfig.failure_modes` by
|
||
|
|
test), applied one level up.
|
||
|
|
|
||
|
|
Two populations, deliberately not summed. They answer different questions and
|
||
|
|
share no denominator:
|
||
|
|
|
||
|
|
signing targets — actor resources in the flex-auth registry snapshot. This is
|
||
|
|
the population the stance map actually governs: `warden sign` resolves a zone
|
||
|
|
per actor resource, and an unresolved one takes the `unknown` cell.
|
||
|
|
routing lanes — catalog entries with an explicit workload reference. Wider than
|
||
|
|
the stance map's reach, and the figure that shows *why* coverage is low: an
|
||
|
|
unknown lane is almost always another repository's undeclared workload
|
||
|
|
identity, which `ADR-0009` rule 3 forbids closing by inference here.
|
||
|
|
|
||
|
|
Usage:
|
||
|
|
python scripts/report_coverage.py [--json]
|
||
|
|
|
||
|
|
Exit: 0 always. This reports; it does not gate. Coverage is disclosure, never a
|
||
|
|
transitional licence, and a script that failed on low coverage would be arguing
|
||
|
|
the case v0.8 declined.
|
||
|
|
"""
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import argparse
|
||
|
|
import json
|
||
|
|
import sys
|
||
|
|
from pathlib import Path
|
||
|
|
from typing import Any
|
||
|
|
|
||
|
|
REPO = Path(__file__).resolve().parent.parent
|
||
|
|
sys.path.insert(0, str(REPO / "src"))
|
||
|
|
|
||
|
|
SNAPSHOT = REPO / "registry" / "flex-auth" / "production_registry_snapshot.json"
|
||
|
|
NOT_APPLICABLE = "not-applicable"
|
||
|
|
|
||
|
|
|
||
|
|
def signing_target_coverage(snapshot_path: Path = SNAPSHOT) -> dict[str, int]:
|
||
|
|
"""Zone resolution across the actor resources `warden sign` can name."""
|
||
|
|
registry = json.loads(snapshot_path.read_text())
|
||
|
|
resolved = unknown = not_applicable = 0
|
||
|
|
for manifest in registry.get("resource_manifests") or []:
|
||
|
|
for resource in manifest.get("resources") or []:
|
||
|
|
if str(resource.get("type")) != "ssh-certificate":
|
||
|
|
continue
|
||
|
|
attributes = resource.get("attributes") or {}
|
||
|
|
admission = str(attributes.get("security_zone_admission") or "unknown")
|
||
|
|
zone = str(attributes.get("security_zone") or "unknown")
|
||
|
|
if admission == NOT_APPLICABLE:
|
||
|
|
not_applicable += 1
|
||
|
|
elif zone == "unknown":
|
||
|
|
unknown += 1
|
||
|
|
else:
|
||
|
|
resolved += 1
|
||
|
|
return {"resolved": resolved, "unknown": unknown, "not_applicable": not_applicable}
|
||
|
|
|
||
|
|
|
||
|
|
def routing_lane_coverage(estate_root: Path | None = None) -> dict[str, int]:
|
||
|
|
"""Workload resolution across catalog lanes.
|
||
|
|
|
||
|
|
Delegates to `report_workload_join.build` rather than re-deriving the join: two
|
||
|
|
implementations of "is this lane resolved" would drift, and the published figure
|
||
|
|
should be the one the join report shows.
|
||
|
|
"""
|
||
|
|
import importlib.util
|
||
|
|
|
||
|
|
spec = importlib.util.spec_from_file_location(
|
||
|
|
"report_workload_join", Path(__file__).resolve().parent / "report_workload_join.py"
|
||
|
|
)
|
||
|
|
join = importlib.util.module_from_spec(spec)
|
||
|
|
spec.loader.exec_module(join)
|
||
|
|
|
||
|
|
report = join.build(
|
||
|
|
REPO / "registry" / "routing" / "catalog.yaml",
|
||
|
|
estate_root if estate_root is not None else Path.home(),
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"resolved": len(report["resolved"]),
|
||
|
|
"unknown": len(report["unknown"]),
|
||
|
|
"not_applicable": len(report["not_applicable"]),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def measure(estate_root: Path | None = None) -> dict[str, Any]:
|
||
|
|
return {
|
||
|
|
"signing_targets": signing_target_coverage(),
|
||
|
|
"routing_lanes": routing_lane_coverage(estate_root),
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
def main(argv: list[str] | None = None) -> int:
|
||
|
|
parser = argparse.ArgumentParser(description=__doc__)
|
||
|
|
parser.add_argument("--json", action="store_true", dest="as_json")
|
||
|
|
args = parser.parse_args(argv)
|
||
|
|
|
||
|
|
report = measure()
|
||
|
|
if args.as_json:
|
||
|
|
print(json.dumps(report, indent=2))
|
||
|
|
return 0
|
||
|
|
|
||
|
|
for name, counts in report.items():
|
||
|
|
total = sum(counts.values())
|
||
|
|
scoped = counts["resolved"] + counts["unknown"]
|
||
|
|
print(f"{name.replace('_', ' ')}: {counts['resolved']}/{scoped} resolved "
|
||
|
|
f"({counts['unknown']} unknown, {counts['not_applicable']} not-applicable, "
|
||
|
|
f"{total} total)")
|
||
|
|
print()
|
||
|
|
print("Coverage is disclosure, not a transitional licence (v0.8 §6.4 obligation 3).")
|
||
|
|
print("Publish in pep-stance.yaml; §13.1 does not compute it for you.")
|
||
|
|
return 0
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
raise SystemExit(main())
|