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
152 lines
5.5 KiB
Python
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())
|