risk-nexus/tools/risk.py
tegwick 2d60312bd1 Self-documenting Makefile and read-only query CLI
`make` now lists targets grouped by use with examples. New read-only
tools/risk.py backs list/all/show/waits/escalations/policies; checked accepts
ID/OUTCOME/NOTE (ARGS still works). Adds CLI tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 6903@bnt-lap001
Assistant-Session: 8319e8a8-ffa6-4eb3-b8bf-b29945628f89
2026-09-22 10:19:49 +02:00

105 lines
3.9 KiB
Python

#!/usr/bin/env python3
"""Read-only query CLI over the register: list, show, waits, escalations, policies.
python3 tools/risk.py list [--all] [--severity high] [--system qonto-assistant]
python3 tools/risk.py show RISK-F-0012
python3 tools/risk.py waits
python3 tools/risk.py escalations
python3 tools/risk.py policies
The verbs are generic on purpose (list/show over records), so that a future
common nexus interface can map onto them. Writing stays with record_check.py.
"""
from __future__ import annotations
import argparse
import sys
import register_lib as lib
SHOW_FIELDS = ("title", "status", "severity", "impact", "likelihood", "disclosure",
"system", "fix_owner", "fix_tracking", "closure_condition", "escalation",
"escalation_status", "cadence", "next_check", "ruling")
def cmd_list(a) -> int:
fs = lib.findings()
if not a.all:
fs = [f for f in fs if lib.watched(f.get("status"))]
if a.severity:
fs = [f for f in fs if f.get("severity") == a.severity]
if a.system:
fs = [f for f in fs if f.get("system") == a.system]
fs.sort(key=lambda f: (lib.sev_rank(f.get("severity")), f["id"]))
if not fs:
print("no matching findings")
for f in fs:
print(f"{f['id']} {str(f.get('severity')):8} {str(f.get('status')):9} "
f"{str(f.get('fix_owner')):20} {f.get('title')}")
return 0
def cmd_show(a) -> int:
rec = next((r for r in lib.findings() + lib.regulatory() if r.get("id") == a.id), None)
if rec is None:
print(f"{a.id}: not found")
return 1
print(f"{a.id} ({rec['_path'].relative_to(lib.REPO)})")
for key in SHOW_FIELDS:
if rec.get(key) not in (None, ""):
print(f" {key:18} {rec[key]}")
for w in rec.get("waiting_on") or []:
print(f" waiting on {w.get('who')} until {w.get('default_at')}: {w.get('what')}")
return 0
def cmd_waits(_a) -> int:
rows = []
for r in lib.records_with_waits():
for w in r["waiting_on"]:
rows.append((str(w.get("default_at")), r.get("id"), w.get("who"), w.get("what")))
for when, rid, who, what in sorted(rows):
print(f"{when} {rid:14} {who:20} {what}")
if not rows:
print("nobody is being waited on")
return 0
def cmd_escalations(_a) -> int:
rows = [f for f in lib.findings() if f.get("escalation") not in (None, "none")]
for f in rows:
print(f"{f['id']} trigger {f.get('escalation_trigger')} {f.get('escalation_status')}"
f" sent {f.get('escalation_sent', '-')} {f.get('title')}")
if not rows:
print("no escalations recorded")
return 0
def cmd_policies(_a) -> int:
for r in lib.regulatory():
print(f"{r.get('id'):14} {str(r.get('status')):22} next {str(r.get('next_check'))[:16]:16} "
f"{r.get('title')}")
return 0
def main(argv: list[str]) -> int:
p = argparse.ArgumentParser(prog="risk", description=__doc__.splitlines()[0])
sub = p.add_subparsers(dest="cmd", required=True)
ls = sub.add_parser("list", help="findings, most severe first (live ones unless --all)")
ls.add_argument("--all", action="store_true", help="include fixed and withdrawn")
ls.add_argument("--severity", choices=lib.SEVERITIES)
ls.add_argument("--system")
ls.set_defaults(fn=cmd_list)
sh = sub.add_parser("show", help="one finding or regulatory record")
sh.add_argument("id")
sh.set_defaults(fn=cmd_show)
for name, fn, hlp in (("waits", cmd_waits, "everything owed to or by the register, by deadline"),
("escalations", cmd_escalations, "escalations and their delivery state"),
("policies", cmd_policies, "regulatory records and full policies")):
sub.add_parser(name, help=hlp).set_defaults(fn=fn)
a = p.parse_args(argv)
return a.fn(a)
if __name__ == "__main__":
raise SystemExit(main(sys.argv[1:]))