T01 fix tracking now reads the owner's workplan file and found two findings the register should have known about. T02 incident and external report intake, the latter routed since the address is not ours to create. T03 the production transition defined by what is held rather than what was announced. T04 the README stops claiming a surface. T05 escalation carries a delivery state and is raised once when unacknowledged. T06 checked_by and a heartbeat, so a 1q rung cannot silently mean nobody looked. T07 coverage: 7 of 117 repos have ever appeared in a finding. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.4 KiB
Python
92 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Record the outcome of a check and move the finding along the ladder.
|
|
|
|
python3 tools/record_check.py RISK-F-0002 clean
|
|
python3 tools/record_check.py RISK-F-0002 moved "flex-auth shipped the fix"
|
|
python3 tools/record_check.py RISK-F-0002 defer 2026-09-01 "operator: after the migration"
|
|
|
|
`clean` climbs one rung, `moved` resets to `instant`, `defer` parks it until a
|
|
date by explicit operator decision. Writes the front-matter and appends a dated
|
|
line to the finding's Reviews section — the check is not recorded until it is
|
|
written down, which is the same rule the register applies to everyone else.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import datetime as dt
|
|
import os
|
|
import re
|
|
import sys
|
|
|
|
import register_lib as lib
|
|
|
|
|
|
def fail(msg: str) -> None:
|
|
print(msg)
|
|
raise SystemExit(2)
|
|
|
|
|
|
def main(argv: list[str]) -> int:
|
|
if len(argv) < 2:
|
|
fail(__doc__)
|
|
fid, outcome, rest = argv[0], argv[1], argv[2:]
|
|
matches = [f for f in lib.findings() + lib.regulatory() if f.get("id") == fid]
|
|
if not matches:
|
|
fail(f"no finding with id {fid}")
|
|
f = matches[0]
|
|
path = f["_path"]
|
|
text = path.read_text(encoding="utf-8")
|
|
now = lib.now()
|
|
stamp = now.strftime("%Y-%m-%dT%H:%M:%SZ")
|
|
rung = f.get("cadence", "instant")
|
|
|
|
if outcome == "clean":
|
|
new_rung = lib.climb(rung)
|
|
streak = int(f.get("clean_streak", 0)) + 1
|
|
nxt = now + lib.interval(new_rung)
|
|
note = rest[0] if rest else "checked, nothing moved"
|
|
line = f"- **{now:%Y-%m-%d}** — clean check: {note}. Cadence {rung} → {new_rung} ({streak} clean in a row); next check {nxt:%Y-%m-%d %H:%MZ}."
|
|
defer = ""
|
|
elif outcome == "moved":
|
|
if not rest:
|
|
fail("`moved` needs a reason: what changed")
|
|
new_rung, streak, nxt = lib.reset(), 0, now
|
|
line = f"- **{now:%Y-%m-%d}** — not clean: {rest[0]} Cadence {rung} → instant; checked again immediately."
|
|
defer = ""
|
|
elif outcome == "defer":
|
|
if len(rest) < 2:
|
|
fail("`defer` needs a date and the operator's reason")
|
|
until, why = rest[0], rest[1]
|
|
new_rung, streak, nxt = rung, int(f.get("clean_streak", 0)), lib.moment(until)
|
|
line = f"- **{now:%Y-%m-%d}** — deferred to {until} by explicit operator decision: {why}"
|
|
defer = until
|
|
else:
|
|
fail("outcome must be one of: clean, moved, defer")
|
|
|
|
subs = {
|
|
"checked_by": os.environ.get("RISK_CHECKED_BY", os.environ.get("USER", "unknown")),
|
|
"last_checked": stamp,
|
|
"next_check": nxt.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
"cadence": new_rung,
|
|
"clean_streak": str(streak),
|
|
}
|
|
if defer:
|
|
subs["deferred_to"] = defer
|
|
for key, value in subs.items():
|
|
quoted = f'"{value}"' if key not in ("cadence", "clean_streak") else value
|
|
if re.search(rf"(?m)^{key}:", text):
|
|
text = re.sub(rf"(?m)^{key}:.*$", f"{key}: {quoted}", text)
|
|
else:
|
|
text = text.replace("\n---\n", f"\n{key}: {quoted}\n---\n", 1)
|
|
|
|
if "\n## Reviews\n" in text:
|
|
text = text.rstrip("\n") + "\n" + line + "\n"
|
|
else:
|
|
text = text.rstrip("\n") + "\n\n## Reviews\n\n" + line + "\n"
|
|
path.write_text(text, encoding="utf-8")
|
|
print(f"{fid}: {outcome} — cadence {rung} → {new_rung}, next check {subs['next_check']}")
|
|
print("REGISTER.md is stale; run `make register`.")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main(sys.argv[1:]))
|