diff --git a/Makefile b/Makefile index 64078f6..f4c2ddf 100644 --- a/Makefile +++ b/Makefile @@ -1,13 +1,14 @@ PY := python3 TOOLS := tools -.PHONY: help register check checked due fixes +.PHONY: help register check checked due fixes coverage help: @echo "make register - rebuild REGISTER.md from findings/" @echo "make check - verify the index, report what is going quiet, and read the inbox" @echo "make due - just the work list: what needs a check right now" @echo "make fixes - the state of every fix this register claims to track" + @echo "make coverage - what the register has never heard from" @echo "make checked - record a check outcome: make checked ARGS=\"RISK-F-0002 clean\"" register: @@ -30,3 +31,6 @@ due: fixes: @$(PY) $(TOOLS)/fix_tracker.py + +coverage: + @$(PY) $(TOOLS)/coverage.py diff --git a/REGISTER.md b/REGISTER.md index 694da4e..b950082 100644 --- a/REGISTER.md +++ b/REGISTER.md @@ -14,7 +14,7 @@ Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. La | [RISK-F-0006](findings/RISK-F-0006-apps-pg-no-backup-configured.md) | apps-pg has no backup configured at all: R0 means no recovery | railiance-platform | **high** | embargoed | **answered** (t3, approved) | railiance-platform | open | 1h (1) | **due** | | [RISK-F-0005](findings/RISK-F-0005-audit-core-unfiltered-read-path.md) | audit-core read path applies no tenant filter; the bound is deployment, not code | audit-core | medium | public | none | audit-core | mitigated | instant (0) | **due** | | [RISK-F-0004](findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md) | tenant-engine events() returns the entire event log unfiltered | tenant-engine | **high** | embargoed | none | tenant-engine | open | 1h (1) | **due** | -| [RISK-F-0003](findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md) | ops-warden agent read-boundary does not fire on ungraded catalog lanes | ops-warden | medium | embargoed | none | ops-warden | mitigated | 1h (1) | **due** | +| [RISK-F-0003](findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md) | ops-warden agent read-boundary does not fire on ungraded catalog lanes | ops-warden | medium | embargoed | none | ops-warden | mitigated | 8h (2) | 2026-08-21 14:32Z | | [RISK-F-0002](findings/RISK-F-0002-ops-warden-sign-ungated.md) | ops-warden signs SSH certificates with no authorization decision, and its unblock is now unsafe | ops-warden | medium | embargoed | **withdrawn** (t6, withdrawn-hazard-window-closed) | ops-warden | open | instant (0) | **due** | | [RISK-F-0001](findings/RISK-F-0001-flex-auth-unauthenticated-check.md) | flex-auth /v1/check authenticates no caller | flex-auth | **high** | public | **withdrawn** (t1, withdrawn-before-sending) | flex-auth | fixed | instant (0) | **due** | diff --git a/docs/method/check-procedure.md b/docs/method/check-procedure.md index 7284cd5..efc04c6 100644 --- a/docs/method/check-procedure.md +++ b/docs/method/check-procedure.md @@ -43,6 +43,7 @@ Then, per due finding: Then record it, in the same sitting: ``` +RISK_CHECKED_BY= \ make checked ARGS="RISK-F-0002 clean" make checked ARGS="RISK-F-0002 moved 'ops-warden enabled the gate'" make checked ARGS="RISK-F-0002 defer 2026-09-01 'operator: after the migration'" diff --git a/docs/method/escalation.md b/docs/method/escalation.md index 0309815..d12d0d3 100644 --- a/docs/method/escalation.md +++ b/docs/method/escalation.md @@ -184,6 +184,26 @@ The test therefore applies to intake, not to sweeps: If that trips, the first suspects are triggers 3 and 4 — spend thresholds set too low, and "unowned" being read where "not yet routed" is meant. +## Delivery is a state, not an act + +`RISK-WP-0005-T05`. An escalation that nobody acknowledged is indistinguishable +from one never sent — which is precisely the failure this register committed +on 2026-08-19 and then fixed for its **own** inbox with an hourly watch, while +leaving the path that matters more unguarded. + +So an escalation carries a state: + +```yaml +escalation_status: pending-operator # sent | seen | answered | withdrawn +escalation_sent: "2026-08-19" +``` + +`make check` reports how long each has been unacknowledged. At seven days it +says so and the escalation is **raised once more** — once, per the rule above. +After that the default applies and is recorded. Repetition until someone +answers is how the operator becomes the queue, and silence that is recorded is +not the same as silence that is ignored. + ## Escalations are batched Four escalations are one conversation, not four interruptions. Open items go diff --git a/docs/method/review.md b/docs/method/review.md index 015efd0..7173344 100644 --- a/docs/method/review.md +++ b/docs/method/review.md @@ -49,6 +49,17 @@ Two rules and one escape: the only way out of the bottom rung other than a clean check, and it is a decision with a name on it rather than a check quietly not happening. +### Who checked, and when anyone last did + +`RISK-WP-0005-T06`. Two defences against the rung telling a lie: + +- **`checked_by` on every check.** `record_check.py` writes it. A rung earned + by nobody in particular is visible as such. +- **A heartbeat.** If nothing anywhere in the register has been checked for two + days, `make check` says so **before anything else**. A `1q` rung means + "stable for a quarter" and "nobody looked for a quarter", and those read + identically from the outside — the heartbeat is what separates them. + ### The rung is the signal This is the point of the design, not a side effect. **The cadence a finding diff --git a/findings/RISK-F-0001-flex-auth-unauthenticated-check.md b/findings/RISK-F-0001-flex-auth-unauthenticated-check.md index a9030f0..fd8e94b 100644 --- a/findings/RISK-F-0001-flex-auth-unauthenticated-check.md +++ b/findings/RISK-F-0001-flex-auth-unauthenticated-check.md @@ -27,9 +27,9 @@ publication_subtitle: "The estate's authorization oracle authenticated no caller revision: "graded-1" last_reviewed: "2026-08-20" review_interval: 6m -embargo_condition: "met 2026-08-19 — FLEX-WP-0015 finished, live probes return 401" -embargo_since: "2026-08-19" -embargo_review: "2026-08-19" +embargo_lifted: "2026-08-19 — FLEX-WP-0015 finished, live probes return 401" +embargo_was_since: "2026-08-19" + escalation: withdrawn escalation_trigger: 1 escalation_status: withdrawn-before-sending diff --git a/findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md b/findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md index c558d5b..75b1e82 100644 --- a/findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md +++ b/findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md @@ -24,12 +24,13 @@ embargo_condition: "RISK-F-0009 resolved — the OpenBao deny set covers every h embargo_since: "2026-08-19" embargo_review: "2026-09-18" escalation: none -last_checked: "2026-08-20T10:02:42Z" -next_check: "2026-08-20T11:02:42Z" -cadence: 1h -clean_streak: 1 +last_checked: "2026-08-21T06:32:10Z" +next_check: "2026-08-21T14:32:10Z" +cadence: 8h +clean_streak: 2 graded_by: risk-nexus ruling: RISK-RULING-2026-08-19 +checked_by: "worsch" --- # RISK-F-0003 — the agent read-boundary has a fourteen-lane blind spot @@ -226,3 +227,4 @@ finding's own fix has landed. `ops-warden`'s report and this register has not probed it. Their operator token is expired, which is also why `RISK-F-0009` rests on a file comparison. - **2026-08-20** — clean check: checked against the inbox and the owner's record; nothing moved. Cadence instant → 1h (1 clean in a row); next check 2026-08-20 11:02Z. +- **2026-08-21** — clean check: fix state read from the owner's file: WARDEN-WP-0032-T05 done; nothing else moved. Cadence 1h → 8h (2 clean in a row); next check 2026-08-21 14:32Z. diff --git a/findings/RISK-F-0005-audit-core-unfiltered-read-path.md b/findings/RISK-F-0005-audit-core-unfiltered-read-path.md index f9d5079..39dd6cc 100644 --- a/findings/RISK-F-0005-audit-core-unfiltered-read-path.md +++ b/findings/RISK-F-0005-audit-core-unfiltered-read-path.md @@ -22,9 +22,9 @@ fidelity_modifier: false production_rescore: true disclosure: public publication: pending-handover -embargo_condition: "met 2026-08-18 — AUDIT-WP-0008-T04 reads done in audit-core's workplan" -embargo_since: "2026-08-19" -embargo_review: "2026-11-17" +embargo_lifted: "2026-08-18 — AUDIT-WP-0008-T04 reads done in audit-core's workplan" +embargo_was_since: "2026-08-19" + escalation: none last_checked: "2026-08-21T06:29:38Z" next_check: "2026-08-21T06:29:38Z" diff --git a/notes/RISK-N-0003-defects-found-by-reading-not-monitoring.md b/notes/RISK-N-0003-defects-found-by-reading-not-monitoring.md index 8e46fa8..df90dcd 100644 --- a/notes/RISK-N-0003-defects-found-by-reading-not-monitoring.md +++ b/notes/RISK-N-0003-defects-found-by-reading-not-monitoring.md @@ -32,3 +32,23 @@ anything nobody thought to look at. It comes back the first time a finding arrives that monitoring plausibly should have caught and did not. That is the evidence this note is missing, and until then filing it would be the register asserting a conclusion it cannot support. + +## Update — 2026-08-21: the denominator + +`RISK-WP-0005-T07` built the smallest possible coverage report, and it puts a +number on what this note could previously only gesture at: + +> **7 of 117 registered repos have ever appeared in a finding. 110 never have.** + +That is not 110 clean repos and this note does not claim it is. It is 110 repos +about which the register knows nothing, and the estate's own evidence — two for +two on tenant boundaries, four defects in one review round — says that looking +tends to find something. + +The note stays a note. There is still no owner for "the estate's ability to +notice its own defects", and nothing here is a defect to route. What changed is +that the gap now has a size, and `make coverage` prints it, which is the +difference between an argument and a measurement. + +It comes back as a finding the first time something is found in one of the 110 +that a reasonable sweep would have caught earlier. diff --git a/tools/__pycache__/fix_tracker.cpython-312.pyc b/tools/__pycache__/fix_tracker.cpython-312.pyc new file mode 100644 index 0000000..b115ce0 Binary files /dev/null and b/tools/__pycache__/fix_tracker.cpython-312.pyc differ diff --git a/tools/coverage.py b/tools/coverage.py new file mode 100644 index 0000000..59f0e2f --- /dev/null +++ b/tools/coverage.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""What has never been looked at. + +RISK-WP-0005-T07, deliberately minimal. The register knows what was reported. +Without this it has no view of what was never assessed, so a system with zero +findings is indistinguishable from a system nobody has examined — while +RISK-N-0003 records that every repo which *has* examined its own boundary this +month found a defect. + +This does not assess anything and does not grade anyone. It counts. +""" +from __future__ import annotations + +import json +import urllib.error +import urllib.request + +import register_lib as lib + +HUB = "http://127.0.0.1:8000/repos/" + + +def repos() -> list[dict] | None: + for url in (HUB, "http://127.0.0.1:8000/repos"): + try: + with urllib.request.urlopen(url, timeout=8) as r: + data = json.load(r) + return data if isinstance(data, list) else data.get("items", []) + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError): + continue + return None + + +def main() -> int: + all_repos = repos() + seen: dict[str, list[str]] = {} + for f in lib.findings(): + for key in (f.get("system"), f.get("fix_owner"), f.get("reported_by")): + if key and isinstance(key, str): + seen.setdefault(key, []).append(f["id"]) + + print("Coverage — what the register has heard from\n") + print(f" systems that have produced or carried a finding: {len(seen)}") + for name in sorted(seen): + print(f" {name}: {', '.join(sorted(set(seen[name])))}") + + if all_repos is None: + print("\n Hub unreachable — the denominator is unknown, which is the whole point of this report.") + return 0 + + names = {r.get("slug") for r in all_repos if r.get("slug")} + unheard = sorted(names - set(seen)) + print(f"\n registered repos: {len(names)}") + print(f" never appeared in any finding: {len(unheard)}") + print("\n A repo in that list has either nothing wrong with it or nobody looking.") + print(" This register cannot tell which, and does not guess.\n") + for name in unheard[:40]: + print(f" {name}") + if len(unheard) > 40: + print(f" … and {len(unheard) - 40} more") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/record_check.py b/tools/record_check.py index c4edc54..b138b51 100644 --- a/tools/record_check.py +++ b/tools/record_check.py @@ -13,6 +13,7 @@ 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 @@ -62,6 +63,7 @@ def main(argv: list[str]) -> int: 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, diff --git a/tools/register_check.py b/tools/register_check.py index 1e5ee03..115b215 100644 --- a/tools/register_check.py +++ b/tools/register_check.py @@ -66,7 +66,7 @@ def main() -> int: for ref in (f.get("related") or []): if ref not in ids: malformed.append(f"{fid} — related names {ref}, which does not exist") - if f.get("embargo_condition") and f.get("disclosure") != "embargoed": + if f.get("embargo_condition") and f.get("disclosure") != "embargoed": # a lift is recorded as embargo_lifted malformed.append(f"{fid} — carries an embargo_condition but disclosure is '{f.get('disclosure')}'") if f.get("disclosure") == "embargoed" and not f.get("embargo_condition"): malformed.append(f"{fid} — embargoed with no condition; a hold with no lift is a silence") @@ -159,11 +159,20 @@ def main() -> int: ] section("Embargoed", 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" - ] + # T05 — an escalation nobody acknowledged is indistinguishable from one + # never sent, which is the failure this register fixed for its own inbox + # and not, until now, for the path that matters more. + esc = [] + for f in fs: + if f.get("escalation") != "required" or f.get("escalation_status") == "answered": + continue + sent = lib.moment(f.get("escalation_sent")) + age = f", sent {(NOW - sent).days}d ago" if sent else ", never marked sent" + state = f.get("escalation_status", "unknown") + tail = " ← unacknowledged; raise once more, then record the default" if ( + sent and (NOW - sent).days >= 7 and state not in ("seen", "answered") + ) else "" + esc.append(f"{f['id']} — trigger {f.get('escalation_trigger')}, {state}{age}{tail}") section("Escalations awaiting the operator", esc, "none") rescore = [ @@ -182,6 +191,17 @@ def main() -> int: reg.append(f"{r.get('id')} — due {when:%Y-%m-%d}: {str(r.get('title','')).strip()}") section("Regulatory records due", reg, "none") + # T06 — a 1q rung means "stable for a quarter" and "nobody looked for a + # quarter", and those read identically. The heartbeat separates them. + checks = [m for f in lib.findings() if (m := lib.moment(f.get("last_checked")))] + if checks: + newest = max(checks) + quiet = (NOW - newest).days + if quiet >= 2: + lines.insert(0, "") + lines.insert(0, f" Nothing anywhere in the register has been checked for {quiet} days.") + lines.insert(0, "HEARTBEAT — THE LADDER IS NOT BEING CLIMBED:") + print(f"Register check — {NOW:%Y-%m-%d %H:%MZ}\n{len(fs)} live finding(s)\n") print("\n".join(lines).rstrip()) return 0 diff --git a/workplans/RISK-WP-0005-close-the-intent-gaps.md b/workplans/RISK-WP-0005-close-the-intent-gaps.md index d025340..7e344e0 100644 --- a/workplans/RISK-WP-0005-close-the-intent-gaps.md +++ b/workplans/RISK-WP-0005-close-the-intent-gaps.md @@ -4,7 +4,7 @@ type: workplan title: "Close the gaps between what INTENT claims and what the register can actually do" domain: infotech repo: risk-nexus -status: active +status: finished owner: the-custodian topic_slug: risk-nexus created: "2026-08-21" @@ -45,7 +45,7 @@ plainly at the top: ```task id: RISK-WP-0005-T01 -status: todo +status: done priority: high ``` @@ -69,11 +69,17 @@ whether or not the register has been checking. Where `fix_tracking` is `unset` (`RISK-F-0004`, `RISK-F-0006`, `RISK-F-0009`), that absence is itself the report. +Completed 2026-08-21. `tools/fix_tracker.py`, behind `make fixes` and inside `make check`. Resolves `fix_tracking` against the owning repo's workplan **file** — task-level ids do not exist in the hub, and the file is the ADR-001 source of truth anyway — and uses the file's last commit date as the honest answer to "has this moved", independent of whether the register looked. Archived workplans are searched too, so a finished-and-filed fix does not read as missing. + +**The first run found two things the register should have known.** `RISK-F-0005`: `AUDIT-WP-0008-T04` had read `done` since 2026-08-18 — the fix landed and this repo spent three days not knowing. Now `mitigated`, embargo lifted, `public`. `RISK-F-0002`: both tracked records were closed *before the finding was filed* — `WARDEN-WP-0007` archived 2026-07-08, `FLEX-WP-0007` finished 2026-06-29, against a finding of 2026-08-18 naming `FLEX-WP-0007` as its blocker. Routed as a question to both owners rather than a conclusion. + +Four findings carry no fix tracking at all, which the report now says out loud instead of leaving an empty field. + ### T02 — An intake path for incidents and external reports ```task id: RISK-WP-0005-T02 -status: todo +status: done priority: high ``` @@ -97,11 +103,15 @@ Three pieces, and only the first is this repo's alone: **Non-goal:** a bug bounty, a disclosure programme, or any commitment to outside parties. The ask is a route, not a promise. +Completed 2026-08-21. `docs/method/intake.md`. Incident intake: file first and grade within the hour, `first_observed` because obligations run from it rather than from the grade, `instant` cadence until it is over, and **escalation immediately rather than batched** — the batching rule protects the operator's attention and a 72-hour notification clock outranks that. `RISK-POL-0005` is wired to `first_observed`, which is what starts the clock. Severity gained a live-incident section: `L4` is what that band was always for. + +External report: **no address exists anywhere in the estate** and creating one is not this repo's call. Routed to `the-custodian` and `policy-nexus` with a concrete proposal — RFC 9116 `security.txt` on the published surface, reports routed here, `reported_by: external` — and an explicit non-goal: a route in, not a bounty, a timeline or safe harbour. + ### T03 — Define the production transition ```task id: RISK-WP-0005-T03 -status: todo +status: done priority: high ``` @@ -118,11 +128,15 @@ tell the repos that carry re-score obligations. Custodian decision on the definition; the plumbing is ours. +Completed 2026-08-21 as a proposal; the definition is the custodian's. `docs/method/production-transition.md` defines it by **what is held rather than what was announced**: the first moment any system holds, processes or decides about real external data. Which means it can happen by accident — one real signup, one migrated contact list — and cannot be reversed by deleting the data afterwards. + +Declared by the custodian, never by this register: a risk register that unilaterally declares the estate to be in production has appointed itself. What the register does is notice and ask. Five re-scores, two acceptances ending and six dormant policies activating are listed as what fires on the day. + ### T04 — Stop claiming a surface we do not have ```task id: RISK-WP-0005-T04 -status: todo +status: done priority: medium ``` @@ -137,11 +151,13 @@ the wording rather than editing it. Small, and exactly the class of claim this register grades other repos down for: a stated control that is not there. +Completed 2026-08-21. `README.md` now says the repo serves nothing yet and that publication runs through `policy-nexus`, with three documents waiting for an address. `INTENT.md` is the constitutional document and its amendment is the custodian's — the wording is proposed to them rather than edited here. + ### T05 — Give escalation a delivery guarantee ```task id: RISK-WP-0005-T05 -status: todo +status: done priority: medium ``` @@ -156,11 +172,13 @@ a re-raise once, per the adopted rule's "raised again once" clause. Not a weekly nag; the rule is explicit that repetition until someone answers is how the operator becomes the queue. +Completed 2026-08-21. Escalation carries a delivery state — `sent → seen → answered` — with `escalation_sent` beside it, and `make check` reports how long each has gone unacknowledged. At seven days it says so and the escalation is raised **once** more, per the adopted rule, after which the default applies and is recorded. This is the fix the register applied to its own inbox on 2026-08-19 and had not applied to the path that matters more. + ### T06 — Make a lie about stability impossible to miss ```task id: RISK-WP-0005-T06 -status: todo +status: done priority: medium ``` @@ -177,11 +195,13 @@ Two cheap defences: register for longer than the shortest rung by some margin, `make check` says so first, before anything else. +Completed 2026-08-21. Two defences against the rung lying: `checked_by` recorded on every check (`RISK_CHECKED_BY`, so an agent names itself rather than inheriting a unix login), and a **heartbeat** that prints before anything else in `make check` when nothing anywhere in the register has been checked for two days. A `1q` rung means "stable for a quarter" and "nobody looked for a quarter", and the heartbeat is what separates them. + ### T07 — A coverage model ```task id: RISK-WP-0005-T07 -status: todo +status: done priority: low ``` @@ -200,6 +220,10 @@ a maturity model, and not this repo assessing anyone. **If this task grows past a page it becomes its own workplan.** Coverage is a different problem from triage and should not quietly absorb this one. +Completed 2026-08-21, minimal and within its escape clause. `tools/coverage.py`, behind `make coverage`, counts what the register has heard from. First run: **7 of 117 registered repos have ever appeared in a finding; 110 never have.** + +That is not 110 clean repos and the report says so — it is 110 repos the register knows nothing about, against an estate whose own evidence is that looking tends to find something. Recorded as an update to `RISK-N-0003` rather than promoted: there is still no owner for estate-wide detection and still no defect to route. What changed is that the gap has a size, which is the difference between an argument and a measurement. + ## Non-goals - **No monitoring.** `RISK-N-0003` stands as a note. A register that grows