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:
parent
0ff87c22d8
commit
ba3b3f686d
7 changed files with 217 additions and 13 deletions
6
Makefile
6
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
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
101
tools/fix_tracker.py
Normal 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)
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue