net-kingdom/tools/rotation-lanes/check.py
tegwick 666160a390
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s
NK-WP-0055-T15: credential rotation-lane register with coverage check
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 308701@bnt-lap001
Assistant-Session: 5548e447-444c-4dcc-bed1-05c16eebfe56
2026-10-28 11:01:08 +01:00

152 lines
5.5 KiB
Python

#!/usr/bin/env python3
# /// script
# dependencies = ["PyYAML>=6,<7"]
# ///
"""Check that every known credential has a rotation lane (NK-WP-0055-T15).
Sources of "known credentials", all in this repository:
- service-identity declarations whose custody is not `none` (federated logins hold nothing);
- the backticked secret names in Credential Management v0.2 section 6;
- the numbered rows of the crisis custody list that are not struck through (retired).
Each lane must name its blast radius, verification step and owner lane. NetKingdom owner-lane
references must exist in `workplans/`; other repositories' references are checked for shape only.
Offline and read-only; the register holds names and procedures, never values.
"""
from __future__ import annotations
import argparse
import glob
import re
import sys
from pathlib import Path
import yaml
ROOT = Path(__file__).resolve().parents[2]
CLASSES = {"service", "database", "client-secret", "openbao-issued", "break-glass"}
STATES = {"admitted", "designed", "deferred", "none"}
LEVELS = {"minimal", "low", "medium", "high"}
TASK_REF = re.compile(r"^[A-Z][A-Z-]*-WP-\d{4}(-T\d{2})?$")
def declared_identities(root):
"""(identities that hold a credential, every declared identity)."""
found, every = set(), set()
for path in sorted(
glob.glob(str(root / "tools/service-identity/declarations/*.yaml"))
):
doc = yaml.safe_load(Path(path).read_text())
for ident in doc.get("identities", []):
name = f"{doc['service']}/{ident['name']}"
every.add(name)
if set(ident.get("custody") or []) - {"none"}:
found.add(name)
return found, every
def standard_secrets(root):
text = (root / "canon/standards/credential-management_v0.2.md").read_text()
section = text.split("## 6. Secret Rotation", 1)[1].split("\n## ", 1)[0]
return set(re.findall(r"^\| `([A-Z][A-Z0-9_]+)` \|", section, flags=re.MULTILINE))
def crisis_items(root):
text = (root / "docs/crisis-custody-list.md").read_text()
return {
m.group(1)
for m in re.finditer(r"^\| (\d+[a-z]?) \| (?!~~)", text, flags=re.MULTILINE)
}
def nk_tasks(root):
ids = set()
for path in glob.glob(str(root / "workplans/**/*.md"), recursive=True):
ids.update(
re.findall(
r"^id: (NK-WP-\d{4}(?:-T\d{2})?)$",
Path(path).read_text(),
flags=re.MULTILINE,
)
)
return ids
def check(register, root):
errors = []
lanes = register.get("lanes", [])
seen = {"identities": {}, "credentials": {}, "crisis_items": {}}
known_nk = nk_tasks(root)
for lane in lanes:
lid = lane.get("id", "?")
if lane.get("class") not in CLASSES:
errors.append(f"{lid}: class must be one of {sorted(CLASSES)}")
if lane.get("state") not in STATES:
errors.append(f"{lid}: state must be one of {sorted(STATES)}")
radius = lane.get("blast_radius") or {}
if (
radius.get("level") not in LEVELS
or not radius.get("affects")
or not radius.get("effect")
):
errors.append(f"{lid}: blast_radius needs level, affects and effect")
for field in ("path", "verification"):
if not str(lane.get(field) or "").strip():
errors.append(f"{lid}: {field} is empty")
refs = lane.get("owner_lane") or []
if not refs:
errors.append(f"{lid}: owner_lane is empty")
for ref in refs:
if not TASK_REF.match(ref):
errors.append(f"{lid}: owner_lane {ref!r} is not a workplan or task id")
elif ref.startswith("NK-WP-") and ref not in known_nk:
errors.append(f"{lid}: owner_lane {ref} not found in workplans/")
for key, owners in seen.items():
for item in lane.get(key) or []:
item = str(item)
if item in owners:
errors.append(f"{lid}: {key} {item} already in lane {owners[item]}")
owners[item] = lid
holding, declared = declared_identities(root)
secrets, items = standard_secrets(root), crisis_items(root)
for kind, required, allowed in (
("identities", holding, declared),
("credentials", secrets, secrets),
("crisis_items", items, items),
):
for missing in sorted(required - set(seen[kind])):
errors.append(
f"no lane for {kind[:-1] if kind != 'identities' else 'identity'} {missing}"
)
for extra in sorted(set(seen[kind]) - allowed):
errors.append(
f"lane {seen[kind][extra]} names unknown {kind} entry {extra}"
)
return lanes, errors
def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"register", nargs="?", default=str(ROOT / "tools/rotation-lanes/lanes.yaml")
)
parser.add_argument("--root", default=str(ROOT))
args = parser.parse_args(argv)
lanes, errors = check(
yaml.safe_load(Path(args.register).read_text()), Path(args.root)
)
by_state = {}
for lane in lanes:
by_state[lane.get("state")] = by_state.get(lane.get("state"), 0) + 1
for error in errors:
print(f"ERROR: {error}")
print(
f"rotation lanes: {len(lanes)} ({', '.join(f'{n} {s}' for s, n in sorted(by_state.items()))}); "
f"{len(errors)} error(s)"
)
return 1 if errors else 0
if __name__ == "__main__":
sys.exit(main())