From ba3b3f686daf1ba698e75433d910e9636c544eb7 Mon Sep 17 00:00:00 2001 From: tegwick Date: Fri, 21 Aug 2026 08:29:38 +0200 Subject: [PATCH] RISK-WP-0005-T01: read the fix records, and two findings moved MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix_tracker.py resolves fix_tracking against the owning repo's workplan file — the ADR-001 source of truth — 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 fix that was filed away does not read as missing. First run, three findings it should have known about: RISK-F-0005 — AUDIT-WP-0008-T04 has read done since 2026-08-18. The fix this finding asked for has landed and the register spent three days not knowing. Now mitigated, embargo lifted, disclosure public. Not fixed: that needs a probe, and T05's adversarial evidence artifact still reads wait. 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 that names FLEX-WP-0007 as the blocker. Routed as a question, not a conclusion. Four findings carry no fix tracking at all, which the report now says out loud rather than leaving as an empty field. Co-Authored-By: Claude Opus 5 --- Makefile | 6 +- REGISTER.md | 5 +- WORK-RECORDS.md | 8 ++ .../RISK-F-0002-ops-warden-sign-ungated.md | 54 +++++++++- ...-F-0005-audit-core-unfiltered-read-path.md | 52 +++++++-- tools/fix_tracker.py | 101 ++++++++++++++++++ tools/register_check.py | 4 + 7 files changed, 217 insertions(+), 13 deletions(-) create mode 100644 tools/fix_tracker.py diff --git a/Makefile b/Makefile index c0edfcc..64078f6 100644 --- a/Makefile +++ b/Makefile @@ -1,12 +1,13 @@ PY := python3 TOOLS := tools -.PHONY: help register check checked due +.PHONY: help register check checked due fixes 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 checked - record a check outcome: make checked ARGS=\"RISK-F-0002 clean\"" register: @@ -26,3 +27,6 @@ checked: due: @$(PY) $(TOOLS)/register_check.py | sed -n '/Checks due/,/^$$/p' @$(PY) $(TOOLS)/inbox_check.py + +fixes: + @$(PY) $(TOOLS)/fix_tracker.py diff --git a/REGISTER.md b/REGISTER.md index 830d295..694da4e 100644 --- a/REGISTER.md +++ b/REGISTER.md @@ -1,6 +1,6 @@ # Register -Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built 2026-08-20. +Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built 2026-08-21. 8 live of 9 findings; 3 notes below the floor. @@ -12,7 +12,7 @@ Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. La | [RISK-F-0008](findings/RISK-F-0008-audit-retention-legal-basis-assumed.md) | The legal basis for retaining audit facts against an erasure request has been assumed, never established | audit-core | medium | public | **answered** (t2, answered) | risk-nexus | accepted | instant (0) | **due** | | [RISK-F-0007](findings/RISK-F-0007-unverified-tenant-boundary.md) | No consumer's tenant boundary is verified anywhere | estate | **high** | embargoed | **answered** (t4, assigned) | per-consumer, on request | accepted | instant (0) | **due** | | [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 | embargoed | none | audit-core | 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-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** | @@ -51,7 +51,6 @@ Held from publication with a stated condition. A hold with no moving condition i | RISK-F-0009 | 2026-08-20 | railiance-platform reports the deny set covers every high-risk lane with a KV path (live verification refines the grade, it is not the condition) | — | | RISK-F-0007 | 2026-08-19 | a verification exists for at least one consumer boundary | 2026-09-18 | | RISK-F-0006 | 2026-08-19 | a backup exists and a restore has been demonstrated once | 2026-09-18 | -| RISK-F-0005 | 2026-08-19 | AUDIT-WP-0008-T04 lands a tenant filter in the read path | 2026-11-17 | | RISK-F-0004 | 2026-08-19 | the read path filters by tenant in code | 2026-09-18 | | RISK-F-0003 | 2026-08-19 | RISK-F-0009 resolved — the OpenBao deny set covers every high-risk lane with a KV path | 2026-09-18 | | RISK-F-0002 | 2026-08-19 | FLEX-WP-0015-T02 shipped and ops-warden policy.enabled true in production | 2026-11-17 | diff --git a/WORK-RECORDS.md b/WORK-RECORDS.md index 9635bd2..dea4c71 100644 --- a/WORK-RECORDS.md +++ b/WORK-RECORDS.md @@ -12,6 +12,7 @@ | workplan | RISK-WP-0002 | active | — | workplans/RISK-WP-0002-publication-handover.md | | workplan | RISK-WP-0003 | finished | — | workplans/RISK-WP-0003-regulatory-intake.md | | workplan | RISK-WP-0004 | finished | — | workplans/RISK-WP-0004-run-the-register.md | +| workplan | RISK-WP-0005 | active | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | | task | RISK-WP-0001-T01 | done | — | workplans/RISK-WP-0001-make-the-register-decidable.md | | task | RISK-WP-0001-T02 | done | — | workplans/RISK-WP-0001-make-the-register-decidable.md | | task | RISK-WP-0001-T03 | done | — | workplans/RISK-WP-0001-make-the-register-decidable.md | @@ -33,3 +34,10 @@ | task | RISK-WP-0004-T04 | done | — | workplans/RISK-WP-0004-run-the-register.md | | task | RISK-WP-0004-T05 | done | — | workplans/RISK-WP-0004-run-the-register.md | | task | RISK-WP-0004-T06 | done | — | workplans/RISK-WP-0004-run-the-register.md | +| task | RISK-WP-0005-T01 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | +| task | RISK-WP-0005-T02 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | +| task | RISK-WP-0005-T03 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | +| task | RISK-WP-0005-T04 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | +| task | RISK-WP-0005-T05 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | +| task | RISK-WP-0005-T06 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | +| task | RISK-WP-0005-T07 | todo | — | workplans/RISK-WP-0005-close-the-intent-gaps.md | diff --git a/findings/RISK-F-0002-ops-warden-sign-ungated.md b/findings/RISK-F-0002-ops-warden-sign-ungated.md index 6fe406c..5d89f38 100644 --- a/findings/RISK-F-0002-ops-warden-sign-ungated.md +++ b/findings/RISK-F-0002-ops-warden-sign-ungated.md @@ -29,8 +29,8 @@ embargo_review: "2026-11-17" escalation: withdrawn escalation_trigger: 6 escalation_status: withdrawn-hazard-window-closed -last_checked: "2026-08-20T10:02:41Z" -next_check: "2026-08-20T10:02:41Z" +last_checked: "2026-08-21T06:29:10Z" +next_check: "2026-08-21T06:29:10Z" cadence: instant clean_streak: 0 waiting_on: @@ -274,3 +274,53 @@ reason than the one this finding was filed for. That is the second time in two days that this finding's blocker turned out to be a claim about the world at a date. - **2026-08-20** — not clean: RISK-V-0001 found the flex-auth-ops-warden policy admits no ingress; the live question is now availability, not attestation. Cadence instant → instant; checked again immediately. + +## Check — 2026-08-21: both tracked records were already closed when this was filed + +`RISK-WP-0005-T01` made this register read the fix records it had been writing +down without reading. The first run answered a question nobody had asked: + +| Tracked | State | Last changed | +| --- | --- | --- | +| `WARDEN-WP-0007` | **archived** | 2026-07-08 | +| `FLEX-WP-0007` | **finished** | 2026-06-29 | + +This finding was filed on **2026-08-18** and states that the gate is "blocked +on `FLEX-WP-0007`". That workplan had read `finished` for seven weeks by then. +`flex-auth` restated the same blocker to this register on 2026-08-19 — +"`policy.enabled` remains off (`FLEX-WP-0007`)" — while their own authoritative +record said it was done. + +**The register is not concluding that the blocker is gone.** Three readings +fit and only the owners can say which: the deployment finished and was later +reverted or never applied; "finished" means something narrower than the +sentence implies; or the blocker is genuinely stale and two repos have been +repeating it. + +What can be said without either owner's help is that **the stated blocker has +not matched the stated record since June**, and nothing would have caught that +before this register started reading fix state. + +### This is the third time on the same finding + +`RISK-F-0002` argued, when it was filed, that a blocker is a claim about the +world at a date, and that its own had been invalidated in a day with nothing +re-checking it. Since then: + +1. `RISK-F-0001` was fixed hours after this register graded the ordering hazard; +2. `RISK-V-0001` found a NetworkPolicy that may make enabling the gate fail + closed for an entirely different reason; +3. and now both tracked records turn out to have been closed before the finding + was written. + +The finding was right about the estate and right about itself. Severity is +unchanged at `medium` — none of this changes what the gate does today — but +what is *blocking* it has been wrong in the record for two months. + +### The ask, sharpened + +Previously: probe whether the flex-auth pin admits ingress. Now, additionally: +**what actually blocks `policy.enabled` today?** If the answer is "nothing +except the ingress question", that is a much shorter path than the one both +repos have been describing. +- **2026-08-21** — not clean: Fix tracking read for the first time: WARDEN-WP-0007 archived 2026-07-08, FLEX-WP-0007 finished 2026-06-29 — both closed before the finding was filed. Cadence instant → instant; checked again immediately. 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 579a7a9..f9d5079 100644 --- a/findings/RISK-F-0005-audit-core-unfiltered-read-path.md +++ b/findings/RISK-F-0005-audit-core-unfiltered-read-path.md @@ -2,7 +2,7 @@ id: RISK-F-0005 type: finding title: "audit-core read path applies no tenant filter; the bound is deployment, not code" -status: open +status: mitigated reported_by: audit-core reported_via: flex-auth routed_by: risk-nexus @@ -20,15 +20,16 @@ impact: I3 likelihood: L2 fidelity_modifier: false production_rescore: true -disclosure: embargoed -embargo_condition: "AUDIT-WP-0008-T04 lands a tenant filter in the read path" +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" 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:29:38Z" +next_check: "2026-08-21T06:29:38Z" +cadence: instant +clean_streak: 0 waiting_on: - who: audit-core what: "is may_read false on every production credential, or only on the sender" @@ -85,3 +86,40 @@ No escalation. Known, owned, tracked, moving. Open at review: has `AUDIT-WP-0008-T04` moved; is `may_read: false` still true of every production credential, not only the sender. - **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. + +## Check — 2026-08-21: the fix landed three days ago and nobody here knew + +`RISK-WP-0005-T01`'s first run read `AUDIT-WP-0008-T04` in `audit-core`'s own +workplan file: **`status: done`**, last changed 2026-08-18. + +The task text is the fix this finding asked for — bind the read path to +permitted tenants, `permits_tenant` governing read as it governs write at the +same choke point, tenant-scoping `/v1/events`, `/v1/events/`, +`/v1/dead-letters` and `/v1/secret-findings`, with tests for cross-tenant read +refusal. + +**Three days sat between the fix landing and this register knowing.** Nothing +was wrong with `audit-core`'s conduct — they did the work and recorded it in +the authoritative place. The register was carrying a string it never read, +which is exactly the gap `RISK-WP-0005-T01` exists to close, found on the run +that closed it. + +**`mitigated`, not `fixed`.** The live gap is closed on the owner's record; the +finding is not, for two reasons: + +1. This register has read a workplan status, not a probe. The standard for + `fixed` in `docs/method/review.md` is something concrete about the running + system, and `audit-core` is the one who can supply it. +2. `AUDIT-WP-0008-T05` — the adversarial `E2` evidence artifact, an identity + bound to tenant A demonstrably unable to read tenant B — reads `wait`. The + control exists; the demonstration that it holds does not. + +**The embargo lifts.** Its condition was `AUDIT-WP-0008-T04` landing a tenant +filter, and that is met. The finding describes a read path that no longer +applies no filter, so publishing it no longer shortens anyone's path. +`disclosure: public`, handover pending with the others. + +The `may_read` question — is it false on every production credential, or only +on the sender — stays open and matters less now that the bound is in code +rather than in a flag. +- **2026-08-21** — not clean: AUDIT-WP-0008-T04 reads done in audit-core's workplan since 2026-08-18; embargo condition met, status mitigated. Cadence 1h → instant; checked again immediately. diff --git a/tools/fix_tracker.py b/tools/fix_tracker.py new file mode 100644 index 0000000..7aaca3d --- /dev/null +++ b/tools/fix_tracker.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Read the state of the fixes this register claims to be tracking. + +RISK-WP-0005-T01. Until now `fix_tracking` held ids like `FLEX-WP-0015-T02` +that this repo wrote down and never read, so an owner who had gone quiet and a +fix that had gone quiet were indistinguishable. + +Resolution is against the owning repo's workplan **file**, which is the +ADR-001 source of truth, and the file's last commit date is the honest answer +to "has this moved" — independent of whether this register happened to look. +""" +from __future__ import annotations + +import datetime as dt +import pathlib +import re +import subprocess + +import register_lib as lib + +ESTATE = pathlib.Path("/home/worsch") +WP_ID = re.compile(r"\b([A-Z]+-WP-[0-9]{4})(-T[0-9]{2,3})?\b") + +# How long a fix may sit unchanged before the register says so, by severity. +STALL_DAYS = {"critical": 14, "high": 14, "medium": 30, "low": 60} + + +def find_workplan(wp_id: str) -> pathlib.Path | None: + hits = sorted(ESTATE.glob(f"*/workplans/{wp_id}-*.md")) + if hits: + return hits[0] + # Archived workplans are still the record. A fix that finished and was + # archived must not read as "not found in the estate" — that is the + # register inventing a gap out of a filing convention. + archived = sorted(ESTATE.glob(f"*/workplans/archived/*{wp_id}-*.md")) + return archived[0] if archived else None + + +def last_commit(path: pathlib.Path) -> dt.datetime | None: + try: + out = subprocess.run( + ["git", "-C", str(path.parent.parent), "log", "-1", "--format=%cI", "--", str(path)], + capture_output=True, text=True, timeout=15, + ).stdout.strip() + return lib.moment(out) if out else None + except (subprocess.SubprocessError, OSError): + return None + + +def task_status(path: pathlib.Path, task_id: str) -> str | None: + m = re.search(rf"id: {re.escape(task_id)}\s*\nstatus: (\w+)", path.read_text(encoding="utf-8")) + return m.group(1) if m else None + + +def resolve(fix_tracking: str) -> list[dict]: + """One entry per work-record id named in the finding's fix_tracking.""" + out = [] + for wp, task in WP_ID.findall(str(fix_tracking or "")): + path = find_workplan(wp) + if path is None: + out.append({"id": wp + (task or ""), "state": "not found in the estate", "moved": None}) + continue + fm = lib.load(path) + archived = "archived" in path.parts + full = wp + task if task else wp + state = task_status(path, full) if task else fm.get("status") + out.append({ + "id": full, + "repo": fm.get("repo", path.parent.parent.name), + "state": (state or "unreadable") + (" [archived]" if archived else ""), + "workplan_status": fm.get("status"), + "moved": last_commit(path), + }) + return out + + +def report() -> list[str]: + lines, now = [], lib.now() + for f in lib.findings(): + if not lib.watched(f.get("status")): + continue + tracking = f.get("fix_tracking") + if not tracking or str(tracking).startswith("unset"): + lines.append(f"{f['id']} ({f.get('severity')}) — NO FIX TRACKING; owner {f.get('fix_owner')}") + continue + for r in resolve(tracking): + if r["moved"]: + age = (now - r["moved"]).days + window = STALL_DAYS.get(f.get("severity"), 30) + done = str(r["state"]).lower() in ("done", "finished", "cancel") + flag = "" if done or age <= window else f" ← UNCHANGED {age}d (window {window}d)" + lines.append(f"{f['id']} — {r['id']}: {r['state']}, last changed {r['moved']:%Y-%m-%d}{flag}") + else: + lines.append(f"{f['id']} — {r['id']}: {r['state']}") + return lines + + +if __name__ == "__main__": + print("Fix state, read from the owning repo's workplan file\n") + for line in report(): + print(" ", line) diff --git a/tools/register_check.py b/tools/register_check.py index e474707..1e5ee03 100644 --- a/tools/register_check.py +++ b/tools/register_check.py @@ -9,6 +9,7 @@ from __future__ import annotations import datetime as dt +import fix_tracker import register_lib as lib NOW = lib.now() @@ -143,6 +144,9 @@ def main() -> int: if ref in waiting_ids and f.get("waiting_on"): deep.append(f"{f['id']} waits, and points at {ref} which also waits — depth two, cut one") section("Waiting on someone", waiting, "nothing is waiting on anyone") + # RISK-WP-0005-T01: the fix's own state, read from the owner's workplan + # file rather than from our memory of what they told us. + section("Fix state", fix_tracker.report(), "no finding claims a tracked fix") if due_defaults: section("Defaults now due — apply them", due_defaults, "none") if deep: