RISK-WP-0005-T01: read the fix records, and two findings moved

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 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-21 08:29:38 +02:00
parent 0ff87c22d8
commit ba3b3f686d
7 changed files with 217 additions and 13 deletions

View file

@ -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

View file

@ -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 |

View file

@ -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 |

View file

@ -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.

View file

@ -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/<id>`,
`/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.

101
tools/fix_tracker.py Normal file
View file

@ -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)

View file

@ -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: