RISK-WP-0001 T01-T06,T08: the four instruments, the index, and the first grading
Severity (impact x likelihood, fidelity modifier for controls that lie, headline-vs-constraint, build-mode double grade, the floor), disclosure (publish/embargoed/restricted, and the build-mode deferral re-taken and narrowed with RISK-F-0001 in hand), escalation (the five INTENT triggers settled plus an ordering-hazard trigger the RISK-F-0002 case forced; proposed, awaiting the custodian), review (intervals, what a review is, what missing one produces, the production re-score). Then applied: RISK-F-0001 critical/embargoed/escalated, RISK-F-0002 medium with a high constraint on RISK-F-0001's remediation, filed as a peer and escalated only on the ordering, RISK-F-0003 high/embargoed/no escalation. No unset field remains. REGISTER.md is generated; make check reports overdue, stalled, ungraded and unanswered escalations without changing anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
e268259f94
commit
d5a3953f2e
15 changed files with 1334 additions and 14 deletions
BIN
tools/__pycache__/register_lib.cpython-312.pyc
Normal file
BIN
tools/__pycache__/register_lib.cpython-312.pyc
Normal file
Binary file not shown.
96
tools/register_check.py
Normal file
96
tools/register_check.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Report what the register is not saying out loud.
|
||||
|
||||
RISK-WP-0001-T08. Report only: it writes nothing, changes no field,
|
||||
escalates nothing on its own, and blocks nothing. A human or the custodian
|
||||
acts on the output. Exit code is 0 unless a finding cannot be read.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
|
||||
import register_lib as lib
|
||||
|
||||
TODAY = dt.date.today()
|
||||
|
||||
|
||||
def date(value):
|
||||
if isinstance(value, dt.date):
|
||||
return value
|
||||
if isinstance(value, str) and value not in ("", "unset"):
|
||||
return dt.date.fromisoformat(value)
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
fs = [f for f in lib.findings() if f.get("status") == "open"]
|
||||
lines: list[str] = []
|
||||
|
||||
def section(title: str, rows: list[str], quiet: str) -> None:
|
||||
lines.append(f"{title}:")
|
||||
if rows:
|
||||
lines.extend(f" {r}" for r in rows)
|
||||
else:
|
||||
lines.append(f" {quiet}")
|
||||
lines.append("")
|
||||
|
||||
ungraded = [
|
||||
f"{f['id']} — {', '.join(k for k in lib.GRADED_FIELDS if str(f.get(k, 'unset')) == 'unset')}"
|
||||
for f in fs
|
||||
if any(str(f.get(k, "unset")) == "unset" for k in lib.GRADED_FIELDS)
|
||||
]
|
||||
section("Ungraded", ungraded, "none — every open finding carries a grade")
|
||||
|
||||
overdue = []
|
||||
for f in fs:
|
||||
due = date(f.get("review_by"))
|
||||
if due and TODAY > due:
|
||||
overdue.append(f"{f['id']} — review was due {due} ({(TODAY - due).days}d ago)")
|
||||
section("Overdue review", overdue, "none")
|
||||
|
||||
stale = []
|
||||
for f in fs:
|
||||
seen = date(f.get("last_reviewed"))
|
||||
interval = lib.REVIEW_INTERVAL_DAYS.get(f.get("severity"))
|
||||
if not seen or not interval or f.get("severity") == "low":
|
||||
continue
|
||||
limit = seen + dt.timedelta(days=interval * lib.STALE_MULTIPLIER)
|
||||
if TODAY > limit:
|
||||
stale.append(
|
||||
f"{f['id']} ({f.get('severity')}) — untouched since {seen}; "
|
||||
f"escalation trigger 5 fires (limit was {limit})"
|
||||
)
|
||||
section("Stalled — escalation trigger 5", stale, "none")
|
||||
|
||||
embargo = []
|
||||
for f in fs:
|
||||
if f.get("disclosure") != "embargoed":
|
||||
continue
|
||||
due = date(f.get("embargo_review"))
|
||||
if due and TODAY > due:
|
||||
embargo.append(
|
||||
f"{f['id']} — embargo not re-decided since {due}; lifts when: {f.get('embargo_condition')}"
|
||||
)
|
||||
section("Embargo overdue for re-decision", embargo, "none")
|
||||
|
||||
esc = [
|
||||
f"{f['id']} — trigger {f.get('escalation_trigger')}, {f.get('escalation_status')}"
|
||||
for f in fs
|
||||
if f.get("escalation") == "required" and f.get("escalation_status") != "answered"
|
||||
]
|
||||
section("Escalations awaiting the operator", esc, "none")
|
||||
|
||||
rescore = [
|
||||
f"{f['id']} — {f.get('severity')} now, {f.get('severity_at_production')} at production"
|
||||
for f in fs
|
||||
if f.get("production_rescore")
|
||||
]
|
||||
section("Owed at the production transition", rescore, "none — no finding is graded lower for build mode")
|
||||
|
||||
print(f"Register check — {TODAY}\n{len(fs)} open finding(s)\n")
|
||||
print("\n".join(lines).rstrip())
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
132
tools/register_index.py
Normal file
132
tools/register_index.py
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate REGISTER.md from the finding front-matter.
|
||||
|
||||
RISK-WP-0001-T05. The index is generated because a hand-maintained one
|
||||
drifts and then lies. Edit the findings, re-run `make register`.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import sys
|
||||
|
||||
import register_lib as lib
|
||||
|
||||
TODAY = dt.date.today()
|
||||
|
||||
|
||||
def date(value) -> dt.date | None:
|
||||
if isinstance(value, dt.date):
|
||||
return value
|
||||
if isinstance(value, str) and value not in ("", "unset"):
|
||||
return dt.date.fromisoformat(value)
|
||||
return None
|
||||
|
||||
|
||||
def review_cell(f: dict) -> str:
|
||||
due = date(f.get("review_by"))
|
||||
if due is None:
|
||||
return "—"
|
||||
late = (TODAY - due).days
|
||||
if late > 0:
|
||||
return f"**{due} (overdue {late}d)**"
|
||||
return str(due)
|
||||
|
||||
|
||||
def sev_cell(f: dict) -> str:
|
||||
sev = f.get("severity", "unset")
|
||||
return f"**{sev}**" if sev in ("critical", "high") else sev
|
||||
|
||||
|
||||
def esc_cell(f: dict) -> str:
|
||||
esc = f.get("escalation", "unset")
|
||||
if esc in ("none", "unset"):
|
||||
return esc
|
||||
trigger = f.get("escalation_trigger")
|
||||
status = f.get("escalation_status", "")
|
||||
tail = f" (t{trigger}{', ' + status if status else ''})" if trigger else ""
|
||||
return f"**{esc}**{tail}"
|
||||
|
||||
|
||||
def render() -> str:
|
||||
fs = lib.findings()
|
||||
ns = lib.notes()
|
||||
open_fs = [f for f in fs if f.get("status") == "open"]
|
||||
|
||||
out = [
|
||||
"# Register",
|
||||
"",
|
||||
f"Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built {TODAY}.",
|
||||
"",
|
||||
f"{len(open_fs)} open of {len(fs)} findings; {len(ns)} notes below the floor.",
|
||||
"",
|
||||
"## Findings",
|
||||
"",
|
||||
"| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |",
|
||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||
]
|
||||
for f in fs:
|
||||
out.append(
|
||||
"| [{id}]({path}) | {title} | {system} | {sev} | {disc} | {esc} | {owner} | {status} | {review} |".format(
|
||||
id=f["id"],
|
||||
path=f["_path"].relative_to(lib.REPO),
|
||||
title=str(f.get("title", "")).strip('"'),
|
||||
system=f.get("system", "—"),
|
||||
sev=sev_cell(f),
|
||||
disc=f.get("disclosure", "unset"),
|
||||
esc=esc_cell(f),
|
||||
owner=f.get("fix_owner", "—"),
|
||||
status=f.get("status", "—"),
|
||||
review=review_cell(f),
|
||||
)
|
||||
)
|
||||
|
||||
constraints = [f for f in fs if f.get("constraint")]
|
||||
if constraints:
|
||||
out += ["", "## Constraints", "",
|
||||
"Hazards created by acting in the wrong order. Each binds another finding's remediation.",
|
||||
"", "| From | Binds | Severity | Constraint |", "| --- | --- | --- | --- |"]
|
||||
for f in constraints:
|
||||
out.append("| {id} | {on} | **{sev}** | {text} |".format(
|
||||
id=f["id"], on=f.get("constraint_on", "—"),
|
||||
sev=f.get("constraint_severity", "—"), text=f["constraint"]))
|
||||
|
||||
embargoed = [f for f in fs if f.get("disclosure") == "embargoed"]
|
||||
if embargoed:
|
||||
out += ["", "## Embargoes", "",
|
||||
"Held from publication with a stated condition. A hold with no moving condition is a stall.",
|
||||
"", "| Finding | Since | Lifts when | Re-decided |", "| --- | --- | --- | --- |"]
|
||||
for f in embargoed:
|
||||
out.append("| {id} | {since} | {cond} | {rev} |".format(
|
||||
id=f["id"], since=f.get("embargo_since", "—"),
|
||||
cond=f.get("embargo_condition", "—"), rev=f.get("embargo_review", "—")))
|
||||
|
||||
if ns:
|
||||
out += ["", "## Notes (below the floor)", "",
|
||||
"Seen, deliberately not findings. Not graded, not reviewed, not published.",
|
||||
"", "| ID | Note | Why below the floor |", "| --- | --- | --- |"]
|
||||
for n in ns:
|
||||
out.append("| [{id}]({path}) | {title} | {why} |".format(
|
||||
id=n["id"], path=n["_path"].relative_to(lib.REPO),
|
||||
title=str(n.get("title", "")).strip('"'),
|
||||
why=str(n.get("floor_reason", "—")).strip('"')))
|
||||
|
||||
out += ["", "## How to read this", "",
|
||||
"Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;",
|
||||
"escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.",
|
||||
"A constraint may be graded higher than the finding that carries it — read both.",
|
||||
""]
|
||||
return "\n".join(out)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
text = render()
|
||||
target = lib.REPO / "REGISTER.md"
|
||||
if "--check" in sys.argv:
|
||||
current = target.read_text(encoding="utf-8") if target.exists() else ""
|
||||
if current != text:
|
||||
print("REGISTER.md is stale — run `make register`")
|
||||
raise SystemExit(1)
|
||||
print("REGISTER.md is current")
|
||||
else:
|
||||
target.write_text(text, encoding="utf-8")
|
||||
print(f"wrote {target.relative_to(lib.REPO)}")
|
||||
42
tools/register_lib.py
Normal file
42
tools/register_lib.py
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
"""Shared front-matter reading for the register tools.
|
||||
|
||||
The finding files are the source of truth. Nothing here writes to them.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import pathlib
|
||||
import yaml
|
||||
|
||||
REPO = pathlib.Path(__file__).resolve().parent.parent
|
||||
FINDINGS = REPO / "findings"
|
||||
NOTES = REPO / "notes"
|
||||
|
||||
SEVERITIES = ["critical", "high", "medium", "low"]
|
||||
REVIEW_INTERVAL_DAYS = {"critical": 7, "high": 30, "medium": 90, "low": 180}
|
||||
STALE_MULTIPLIER = 2
|
||||
GRADED_FIELDS = ["severity", "disclosure", "escalation", "review_by", "last_reviewed"]
|
||||
|
||||
|
||||
def load(path: pathlib.Path) -> dict:
|
||||
text = path.read_text(encoding="utf-8")
|
||||
if not text.startswith("---\n"):
|
||||
raise ValueError(f"{path.name}: no front-matter")
|
||||
_, fm, _body = text.split("---\n", 2)
|
||||
data = yaml.safe_load(fm) or {}
|
||||
data["_path"] = path
|
||||
return data
|
||||
|
||||
|
||||
def findings() -> list[dict]:
|
||||
items = [load(p) for p in sorted(FINDINGS.glob("RISK-F-*.md"))]
|
||||
return sorted(items, key=lambda f: f["id"], reverse=True)
|
||||
|
||||
|
||||
def notes() -> list[dict]:
|
||||
if not NOTES.exists():
|
||||
return []
|
||||
return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True)
|
||||
|
||||
|
||||
def sev_rank(sev: str) -> int:
|
||||
return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)
|
||||
Loading…
Add table
Add a link
Reference in a new issue