Compare commits
No commits in common. "c73848250cc9cc2073b2a659eef28dc9ab0bd41c" and "8b204d0411ed42d2f110a98d75ba248484927863" have entirely different histories.
c73848250c
...
8b204d0411
18 changed files with 148 additions and 524 deletions
31
REGISTER.md
31
REGISTER.md
|
|
@ -2,21 +2,21 @@
|
||||||
|
|
||||||
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-20.
|
||||||
|
|
||||||
8 live of 9 findings; 3 notes below the floor.
|
6 open of 9 findings; 3 notes below the floor.
|
||||||
|
|
||||||
## Findings
|
## Findings
|
||||||
|
|
||||||
| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Cadence | Next check |
|
| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |
|
||||||
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
||||||
| [RISK-F-0009](findings/RISK-F-0009-openbao-deny-set-covers-a-third-of-high-risk-lanes.md) | agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest | railiance-platform | **high** | embargoed | none | railiance-platform | open | instant (0) | **due** |
|
| [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 | **required** (t2, partially-answered) | risk-nexus | open | 2026-11-17 |
|
||||||
| [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 | **required** (t2, partially-answered) | risk-nexus | open | 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 | 2026-09-18 |
|
||||||
| [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 | 2026-09-18 |
|
||||||
| [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 | instant (0) | **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 | 2026-11-17 |
|
||||||
| [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 | instant (0) | **due** |
|
| [RISK-F-0004](findings/RISK-F-0004-agent-boundary-policy-covers-a-third-of-high-risk-lanes.md) | agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest | railiance-platform | unset | unset | unset | railiance-platform | open | — |
|
||||||
| [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 | 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 | 2026-09-18 |
|
||||||
| [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 | instant (0) | **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 | **high** | embargoed | none | ops-warden | mitigated | 2026-09-18 |
|
||||||
| [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-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 | 2026-11-17 |
|
||||||
| [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** |
|
| [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 | 2026-08-26 |
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
|
|
@ -32,12 +32,11 @@ Held from publication with a stated condition. A hold with no moving condition i
|
||||||
|
|
||||||
| Finding | Since | Lifts when | Re-decided |
|
| Finding | Since | Lifts when | Re-decided |
|
||||||
| --- | --- | --- | --- |
|
| --- | --- | --- | --- |
|
||||||
| RISK-F-0009 | 2026-08-20 | the deny set covers every high-risk lane with a KV path, verified against live OpenBao | — |
|
|
||||||
| RISK-F-0007 | 2026-08-19 | a verification exists for at least one consumer boundary | 2026-09-18 |
|
| 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-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-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-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-0003 | 2026-08-19 | the five exec_capable lanes graded under WARDEN-WP-0032-T05 | 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 |
|
| RISK-F-0002 | 2026-08-19 | FLEX-WP-0015-T02 shipped and ops-warden policy.enabled true in production | 2026-11-17 |
|
||||||
|
|
||||||
## Notes (below the floor)
|
## Notes (below the floor)
|
||||||
|
|
@ -53,7 +52,5 @@ Seen, deliberately not findings. Not graded, not reviewed, not published.
|
||||||
## How to read this
|
## How to read this
|
||||||
|
|
||||||
Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;
|
Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;
|
||||||
escalation `docs/method/escalation.md`; the check cadence `docs/method/review.md`.
|
escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.
|
||||||
Cadence is the ladder rung and the count of consecutive clean checks — a finding at `1q (9)`
|
|
||||||
has held still for a long time; one at `instant (0)` moved recently. Anything wrong resets it.
|
|
||||||
A constraint may be graded higher than the finding that carries it — read both.
|
A constraint may be graded higher than the finding that carries it — read both.
|
||||||
|
|
|
||||||
|
|
@ -103,22 +103,33 @@ the operator is a *stuck* disagreement, because only the operator can assign
|
||||||
work across repos that will not take it. One exchange, then escalate — not
|
work across repos that will not take it. One exchange, then escalate — not
|
||||||
three, and not zero.
|
three, and not zero.
|
||||||
|
|
||||||
### 5. Stalled remediation — adopted, bounded by the bottom rung
|
### 5. Stalled remediation — adopted, bounded by interval
|
||||||
|
|
||||||
Escalate when a finding has sat at the **`instant` rung of the cadence ladder
|
Escalate when a finding's fix has not moved for **twice its review interval**
|
||||||
for more than fourteen days** (`docs/method/review.md`).
|
(`docs/method/review.md`): 14 days for `critical`, 60 for `high`, 180 for
|
||||||
|
`medium`. `low` findings do not escalate on staleness; they age out through
|
||||||
|
review.
|
||||||
|
|
||||||
The bottom rung means one of two things: every check keeps finding something
|
"Not moved" means the fix owner's tracking record has not changed state and no
|
||||||
wrong, or no check is happening. Fourteen days of either is a stall, and the
|
new evidence has arrived. A deliberate, stated deferral by the owner is
|
||||||
escalation does not have to know which — the operator will.
|
movement. Silence is not.
|
||||||
|
|
||||||
This replaces the original severity-keyed interval. It is strictly better: a
|
### 6. Ordering hazard across repos — added
|
||||||
`medium` finding that keeps resetting is stalling visibly, and the old rule
|
|
||||||
would have waited 180 days to say so.
|
|
||||||
|
|
||||||
`low` findings are not exempt. Under the ladder, severity does not set cadence
|
Escalate once when remediating finding A in the wrong order relative to
|
||||||
at all, so there is no rung a `low` finding sits on that a `critical` one does
|
finding B would create a **false attestation** — a control that starts
|
||||||
not.
|
claiming a check happened when it did not.
|
||||||
|
|
||||||
|
This trigger is not in `INTENT.md`'s list. It is added because `RISK-F-0002`
|
||||||
|
produced the case: enabling the `ops-warden` signing gate against a forgeable
|
||||||
|
`flex-auth` would convert an absent control into a signed audit record
|
||||||
|
asserting an authorization that was never made. Ordering hazards are cheap to
|
||||||
|
avoid before the fact and expensive to detect afterwards, because the evidence
|
||||||
|
is what lies.
|
||||||
|
|
||||||
|
The escalation is a single acknowledgement of the ordering, not a standing
|
||||||
|
supervision request. Once the operator has the constraint, the register carries
|
||||||
|
it.
|
||||||
|
|
||||||
## What does not escalate
|
## What does not escalate
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,75 +14,20 @@ review_interval: 180d
|
||||||
`INTENT.md`: a finding that has sat untouched past its review date is itself a
|
`INTENT.md`: a finding that has sat untouched past its review date is itself a
|
||||||
finding. Silence is not resolution. This document makes that operable.
|
finding. Silence is not resolution. This document makes that operable.
|
||||||
|
|
||||||
## The cadence ladder
|
## Intervals
|
||||||
|
|
||||||
Operator ruling, 2026-08-20. **Intervals are not set by severity. They are
|
Set from the headline severity at grading time, and reset at each review.
|
||||||
earned by outcomes.**
|
|
||||||
|
|
||||||
A finding is checked, and the check comes back clean or it does not. Clean
|
| Severity | Interval | Stale (2×, escalation trigger 5) |
|
||||||
climbs one rung. Anything wrong drops straight back to the bottom.
|
| --- | --- | --- |
|
||||||
|
| `critical` | 7 days | 14 days |
|
||||||
|
| `high` | 30 days | 60 days |
|
||||||
|
| `medium` | 90 days | 180 days |
|
||||||
|
| `low` | 180 days | — (ages out through review) |
|
||||||
|
|
||||||
| Rung | Wait before the next check |
|
A finding graded `critical` is being looked at weekly or it is not being
|
||||||
| --- | --- |
|
managed. If weekly is unbearable, the honest response is to re-grade it, not
|
||||||
| `instant` | now, and again immediately until a check comes back clean |
|
to let the date slide.
|
||||||
| `1h` | one hour |
|
|
||||||
| `8h` | eight hours |
|
|
||||||
| `24h` | one day |
|
|
||||||
| `48h` | two days |
|
|
||||||
| `96h` | four days |
|
|
||||||
| `7d` | one week |
|
|
||||||
| `14d` | two weeks |
|
|
||||||
| `1mo` | one month |
|
|
||||||
| `1q` | one quarter — **the ceiling; nothing is ever checked less often than this** |
|
|
||||||
|
|
||||||
Two rules and one escape:
|
|
||||||
|
|
||||||
- **Climb on clean.** One rung per clean check, never two.
|
|
||||||
- **Reset on anything wrong.** Not a slide down one rung — straight to
|
|
||||||
`instant`. A matter that has just moved has no track record, whatever it had
|
|
||||||
before.
|
|
||||||
- **The operator may defer.** An `instant` finding can be deferred to a stated
|
|
||||||
future date by explicit operator decision, recorded as `deferred_to`. That is
|
|
||||||
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.
|
|
||||||
|
|
||||||
### The rung is the signal
|
|
||||||
|
|
||||||
This is the point of the design, not a side effect. **The cadence a finding
|
|
||||||
sits on is a statement about how stable the estate has been on that matter.**
|
|
||||||
|
|
||||||
`RISK-F-0002` at `1q (9)` says the signing gate has been examined ten times
|
|
||||||
over three months and nothing has moved. `RISK-F-0002` at `instant (0)` says
|
|
||||||
something changed within the day. The number carries information that no
|
|
||||||
severity does — severity says how bad it would be, the rung says how settled
|
|
||||||
it is — and the two are independent. A `low` finding that keeps resetting is
|
|
||||||
telling you something a `critical` one at the ceiling is not.
|
|
||||||
|
|
||||||
It is also self-correcting in the direction that matters. Anything volatile
|
|
||||||
gets attention often, automatically, without anyone deciding it deserves it;
|
|
||||||
anything genuinely quiet stops consuming attention, without anyone deciding to
|
|
||||||
stop looking. Neither of those judgements has to be made by a person who might
|
|
||||||
be wrong or busy.
|
|
||||||
|
|
||||||
### What "clean" means
|
|
||||||
|
|
||||||
A check is **clean** when nothing about the assessment moved: the grade still
|
|
||||||
holds, every stated blocker is still true, the fix state is unchanged, the
|
|
||||||
disclosure state is still right, and no new fact has arrived.
|
|
||||||
|
|
||||||
A check is **not clean** when any of those moved — including when they moved in
|
|
||||||
a good direction. `RISK-F-0001` being fixed is not a clean check; it is a large
|
|
||||||
change, and the next check comes immediately. Good news resets the ladder
|
|
||||||
exactly like bad news, because the ladder measures stillness, not health.
|
|
||||||
|
|
||||||
### Starting position
|
|
||||||
|
|
||||||
Every finding starts at `instant`. A register with no check history has no
|
|
||||||
grounds to wait, and the first clean check is what buys the first hour.
|
|
||||||
|
|
||||||
The whole register sat at `instant (0)` on 2026-08-20, which is correct and
|
|
||||||
temporary: everything in it had been graded, re-graded or ruled on within the
|
|
||||||
preceding day.
|
|
||||||
|
|
||||||
## What a review is
|
## What a review is
|
||||||
|
|
||||||
|
|
@ -113,19 +58,18 @@ and `review_by` is pushed by one interval. A review that changes nothing still
|
||||||
writes the line — "checked, nothing moved" is the evidence that the silence was
|
writes the line — "checked, nothing moved" is the evidence that the silence was
|
||||||
observed rather than accidental.
|
observed rather than accidental.
|
||||||
|
|
||||||
## When a check is missed
|
## When a review is missed
|
||||||
|
|
||||||
Overdue is not a status change on the finding. It is a fact about *this repo*,
|
Overdue is not a status change on the finding. It is a fact about *this repo*,
|
||||||
and it surfaces in three places:
|
and it surfaces in three places:
|
||||||
|
|
||||||
- `make check` lists it under "Checks due", with how late it is and which rung
|
- `make check` reports it (`tools/register_check.py`), always, unconditionally.
|
||||||
it is on.
|
- `REGISTER.md` shows the finding as overdue with the day count.
|
||||||
- `REGISTER.md` shows the next check as **due**.
|
- At **twice** the interval, escalation trigger 5 fires for `critical`,
|
||||||
- A finding sitting at the bottom rung for more than fourteen days with no
|
`high` and `medium`. That is the point at which the register's own silence
|
||||||
movement fires escalation trigger 5. Bottom rung means it keeps failing or
|
reaches the operator.
|
||||||
keeps being skipped; fourteen days of that is a stall whichever it is.
|
|
||||||
|
|
||||||
The register does not auto-escalate severity for lateness and does not
|
The register does not auto-escalate severity for lateness, and it does not
|
||||||
auto-close anything. Both would be the register lying about its own state to
|
auto-close anything. Both would be the register lying about its own state to
|
||||||
make a number look better.
|
make a number look better.
|
||||||
|
|
||||||
|
|
@ -146,17 +90,13 @@ obligation is visible rather than discovered on the day.
|
||||||
## Front-matter this adds
|
## Front-matter this adds
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
last_checked: "2026-08-20T05:40:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-20T06:40:00Z"
|
review_by: "2026-08-26"
|
||||||
cadence: 1h
|
|
||||||
clean_streak: 1
|
|
||||||
production_rescore: true
|
production_rescore: true
|
||||||
deferred_to: "" # only by explicit operator decision
|
|
||||||
```
|
```
|
||||||
|
|
||||||
`next_check` is what the nag reads, and it is an absolute moment rather than a
|
`review_by` is what the nag reads. It is a date, never a duration, so that
|
||||||
duration, so nothing has to recompute an interval to know whether a check is
|
nothing has to recompute an interval to know whether a finding is late.
|
||||||
late. The rungs run in hours as well as days, so it carries a time.
|
|
||||||
|
|
||||||
## Closing a finding
|
## Closing a finding
|
||||||
|
|
||||||
|
|
@ -166,20 +106,10 @@ A finding leaves `open` for exactly one of:
|
||||||
something concrete rather than been told. Publication follows if the
|
something concrete rather than been told. Publication follows if the
|
||||||
disclosure state was `embargoed`.
|
disclosure state was `embargoed`.
|
||||||
- `accepted` — the estate is deliberately carrying it. Requires who accepted
|
- `accepted` — the estate is deliberately carrying it. Requires who accepted
|
||||||
it, why, and what ends the acceptance. `accepted` is not closed: it stays on
|
it, why, and a re-review date. `accepted` is not `closed`; it keeps a review
|
||||||
the ladder forever, and it climbs like anything else.
|
interval forever.
|
||||||
- `mitigated` — the live gap is closed but the finding is not. `RISK-F-0003` is
|
|
||||||
the case: the boundary now fires, and the omission that let it not fire is
|
|
||||||
still there. Stays watched.
|
|
||||||
- `withdrawn` — the finding was wrong, or the defect never existed. Say which.
|
- `withdrawn` — the finding was wrong, or the defect never existed. Say which.
|
||||||
|
|
||||||
**Any status the tooling does not recognise keeps the finding watched, and the
|
|
||||||
unrecognised word is reported.** `RISK-F-0003` arrived as `mitigated` on
|
|
||||||
2026-08-20, before that word existed here, and dropped silently out of
|
|
||||||
`make check` — a finding vanishing from the nag because someone used an
|
|
||||||
unfamiliar word is precisely the failure this register exists to prevent. The
|
|
||||||
tooling now fails loud instead of quiet.
|
|
||||||
|
|
||||||
There is no `stale`, no `wontfix` and no silent expiry. A finding that nobody
|
There is no `stale`, no `wontfix` and no silent expiry. A finding that nobody
|
||||||
will fix and nobody will accept stays `open` and keeps arriving in the nag,
|
will fix and nobody will accept stays `open` and keeps arriving in the nag,
|
||||||
because that is the true state.
|
because that is the true state.
|
||||||
|
|
|
||||||
|
|
@ -1,95 +0,0 @@
|
||||||
---
|
|
||||||
id: RISK-RULING-2026-08-20
|
|
||||||
type: ruling
|
|
||||||
title: "The OpenBao layer, an id collision, and a status the tooling did not know"
|
|
||||||
status: recorded
|
|
||||||
owner: risk-nexus
|
|
||||||
date: "2026-08-20"
|
|
||||||
workplan: RISK-WP-0001-T06
|
|
||||||
---
|
|
||||||
|
|
||||||
# 2026-08-20 — the second ops-warden layer
|
|
||||||
|
|
||||||
`ops-warden` answered the question this register put to them yesterday, and the
|
|
||||||
answer was a finding rather than a yes or no. Three things came out of it: a
|
|
||||||
grading, an id collision, and a defect in this repo's own tooling.
|
|
||||||
|
|
||||||
## RISK-F-0009 — `high`, embargoed, no escalation
|
|
||||||
|
|
||||||
Filed by `ops-warden` as the OpenBao half of `ADR-0004`. The policy
|
|
||||||
`agent-high-risk-boundary` denies 5 data paths, covering 6 of 17 high-risk
|
|
||||||
lanes; **8 high-risk lanes have a concrete KV path nothing denies**, reachable
|
|
||||||
by a direct `bao kv get` that never touches ops-warden.
|
|
||||||
|
|
||||||
Graded `I4` × `L2`. `I4` on the worst uncovered lane rather than the average
|
|
||||||
one: `scaleway-bootstrap` and `agent-harness-forgejo-deploy` propagate past the
|
|
||||||
system holding them. `L2` because it needs an agent token carrying a
|
|
||||||
`workload-kv-read-*` policy, which the estate grants.
|
|
||||||
|
|
||||||
Two limits held rather than resolved, both `ops-warden`'s own: the comparison
|
|
||||||
is against the policy **file**, because their operator token is expired
|
|
||||||
(`bao token lookup` → 403), so the deployed policy may differ; and whether any
|
|
||||||
agent token carries both policies at once is not established.
|
|
||||||
|
|
||||||
**The finding's most useful sentence is the one that declines credit.** Four of
|
|
||||||
the eight uncovered lanes were already `high` before yesterday's regrade, so
|
|
||||||
the divergence is pre-existing; the regrade only made the two lists comparable
|
|
||||||
enough to see it. A register that let this read as "our work found a new
|
|
||||||
defect" would be misreading its own evidence.
|
|
||||||
|
|
||||||
No escalation: two repos, no disagreement. Trigger 4 needs a *failed* routing
|
|
||||||
exchange, and no exchange has failed. Their suggestion — generate the deny set
|
|
||||||
from the catalog rather than maintaining two lists — is routed to
|
|
||||||
`railiance-platform` as the suggestion it was offered as.
|
|
||||||
|
|
||||||
## RISK-F-0003 re-checked: `high` → `medium`, and its embargo re-pointed
|
|
||||||
|
|
||||||
All 17 high-risk lanes now exit 7 on the CLI path. The live gap is closed; the
|
|
||||||
omission that allowed it — a lane addable without a grade — is not, and that is
|
|
||||||
`WARDEN-WP-0032-T06` plus, durably, the zone model (`RISK-N-0004`). `medium`,
|
|
||||||
because "graded by hand once" is not "cannot be ungraded again".
|
|
||||||
|
|
||||||
Its embargo condition was met and **still did not lift**. The original
|
|
||||||
condition was the five lanes being graded; that happened. But `RISK-F-0009`
|
|
||||||
shows the second layer still leaves eight lanes reachable by the direct path,
|
|
||||||
so publishing `RISK-F-0003` now would name lanes an attacker could still use.
|
|
||||||
The condition is re-pointed at `RISK-F-0009`.
|
|
||||||
|
|
||||||
That is `docs/method/disclosure.md` working across findings rather than within
|
|
||||||
one: the test is whether the *text* shortens a path, not whether this
|
|
||||||
particular finding's own fix has landed.
|
|
||||||
|
|
||||||
## The id collision
|
|
||||||
|
|
||||||
`ops-warden` filed as `RISK-F-0004`. This register had assigned `RISK-F-0004`
|
|
||||||
to `tenant-engine`'s unfiltered event read hours earlier, in a commit they had
|
|
||||||
not seen. Two findings, one id, both committed.
|
|
||||||
|
|
||||||
Resolved by the only rule that does not require anyone to have been at fault:
|
|
||||||
**earlier commit keeps the id, the newcomer moves.** `RISK-F-0009`, with
|
|
||||||
`filed_as: RISK-F-0004` recorded on it so their references remain traceable.
|
|
||||||
|
|
||||||
The real fix is in `findings/README.md`, which now says how ids are allocated
|
|
||||||
and what happens on a clash. The contract had never said, so nobody did
|
|
||||||
anything wrong.
|
|
||||||
|
|
||||||
## The tooling defect, which is the one worth remembering
|
|
||||||
|
|
||||||
`RISK-F-0003` came back with `status: mitigated` — an accurate word, and one
|
|
||||||
this register had never defined. `make check` watched `open` and `accepted`,
|
|
||||||
so the finding **silently dropped out of the nag**.
|
|
||||||
|
|
||||||
A finding disappearing from the register's own watch list because a reporter
|
|
||||||
used an unfamiliar word is the exact failure `INTENT.md` names: silence that
|
|
||||||
looks like resolution.
|
|
||||||
|
|
||||||
Two changes:
|
|
||||||
|
|
||||||
- `mitigated` is now a defined status — the live gap closed, the finding not.
|
|
||||||
It was a better word than anything this repo had.
|
|
||||||
- **Any unrecognised status keeps the finding watched and gets reported.**
|
|
||||||
The watch list is now defined by exclusion — only `fixed` and `withdrawn`
|
|
||||||
leave it — so an unknown word can never again quietly remove something.
|
|
||||||
|
|
||||||
Fail loud, not quiet. The register got this wrong on its second day and found
|
|
||||||
it by accident, which is the same way everything else in here has been found.
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
# Filing a finding
|
# Filing a finding
|
||||||
|
|
||||||
One file per finding: `findings/RISK-F-NNNN-<slug>.md`, YAML front-matter,
|
One file per finding: `findings/RISK-F-NNNN-<slug>.md`, YAML front-matter,
|
||||||
then prose.
|
then prose. Next id is one past the highest here.
|
||||||
|
|
||||||
**Ids are allocated by `risk-nexus`.** Take the next id past the highest you
|
|
||||||
can see and file — that is the right thing to do — but if two reporters take
|
|
||||||
the same one, the earlier commit keeps it and the newcomer is renumbered here,
|
|
||||||
with the original id recorded as `filed_as`. This happened on 2026-08-20
|
|
||||||
(`RISK-F-0009`, filed as `RISK-F-0004`), which is why it is written down. Do
|
|
||||||
not renumber your own finding after filing; the register does it and tells you.
|
|
||||||
|
|
||||||
## What the reporter fills in
|
## What the reporter fills in
|
||||||
|
|
||||||
|
|
@ -31,14 +24,8 @@ related: [RISK-F-0001] # optional
|
||||||
|
|
||||||
`severity`, `severity_at_production`, `impact`, `likelihood`,
|
`severity`, `severity_at_production`, `impact`, `likelihood`,
|
||||||
`fidelity_modifier`, `production_rescore`, `disclosure`, `embargo_*`,
|
`fidelity_modifier`, `production_rescore`, `disclosure`, `embargo_*`,
|
||||||
`escalation*`, `constraint*`, `last_checked`, `next_check`, `cadence`,
|
`escalation*`, `constraint*`, `last_reviewed`, `review_by`, `graded_by`,
|
||||||
`clean_streak`, `graded_by`, `ruling`.
|
`ruling`.
|
||||||
|
|
||||||
`cadence` is the check-frequency rung, and it is earned rather than assigned:
|
|
||||||
a clean check climbs one rung (`instant` → `1h` → `8h` → `24h` → `48h` → `96h`
|
|
||||||
→ `7d` → `14d` → `1mo` → `1q`), and anything moving drops it straight back to
|
|
||||||
`instant`. So the rung on your finding is a public statement about how settled
|
|
||||||
the matter has been — which is why it is not yours to set.
|
|
||||||
|
|
||||||
Setting them yourself is not an error to be corrected — it is a boundary this
|
Setting them yourself is not an error to be corrected — it is a boundary this
|
||||||
repo would rather keep. The reporter says what is true; this repo says how bad
|
repo would rather keep. The reporter says what is true; this repo says how bad
|
||||||
|
|
|
||||||
|
|
@ -28,10 +28,8 @@ escalation: withdrawn
|
||||||
escalation_trigger: 1
|
escalation_trigger: 1
|
||||||
escalation_status: withdrawn-before-sending
|
escalation_status: withdrawn-before-sending
|
||||||
date_fixed: "2026-08-19"
|
date_fixed: "2026-08-19"
|
||||||
last_checked: "2026-08-19T21:30:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-19T21:30:00Z" # due now: the ladder starts at instant
|
review_by: "2026-08-26"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19
|
ruling: RISK-RULING-2026-08-19
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,8 @@ embargo_review: "2026-11-17"
|
||||||
escalation: withdrawn
|
escalation: withdrawn
|
||||||
escalation_trigger: 6
|
escalation_trigger: 6
|
||||||
escalation_status: withdrawn-hazard-window-closed
|
escalation_status: withdrawn-hazard-window-closed
|
||||||
last_checked: "2026-08-19T21:30:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-19T21:30:00Z" # due now: the ladder starts at instant
|
review_by: "2026-11-17"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19
|
ruling: RISK-RULING-2026-08-19
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,21 @@ date_reported: "2026-08-19"
|
||||||
system: ops-warden
|
system: ops-warden
|
||||||
environment: production
|
environment: production
|
||||||
fix_owner: ops-warden
|
fix_owner: ops-warden
|
||||||
fix_tracking: WARDEN-WP-0032-T05 (done) / T06 (structural)
|
fix_tracking: WARDEN-WP-0032-T05
|
||||||
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
|
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
|
||||||
severity: medium
|
severity: high
|
||||||
severity_at_production: medium
|
severity_at_production: high
|
||||||
severity_superseded: "high (2026-08-19) — the CLI-layer gap is mitigated"
|
|
||||||
impact: I4
|
impact: I4
|
||||||
likelihood: L2
|
likelihood: L2
|
||||||
fidelity_modifier: false
|
fidelity_modifier: false
|
||||||
production_rescore: false
|
production_rescore: false
|
||||||
disclosure: embargoed
|
disclosure: embargoed
|
||||||
embargo_condition: "RISK-F-0009 resolved — the OpenBao deny set covers every high-risk lane with a KV path"
|
embargo_condition: "the five exec_capable lanes graded under WARDEN-WP-0032-T05"
|
||||||
embargo_since: "2026-08-19"
|
embargo_since: "2026-08-19"
|
||||||
embargo_review: "2026-09-18"
|
embargo_review: "2026-09-18"
|
||||||
escalation: none
|
escalation: none
|
||||||
last_checked: "2026-08-20T05:40:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-20T05:40:00Z" # due now: the ladder starts at instant
|
review_by: "2026-09-18"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19
|
ruling: RISK-RULING-2026-08-19
|
||||||
---
|
---
|
||||||
|
|
@ -193,35 +190,3 @@ Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
|
||||||
Open at review: OpenBao policy coverage of the five paths; whether
|
Open at review: OpenBao policy coverage of the five paths; whether
|
||||||
`WARDEN-WP-0032-T05` has landed; whether any compliance evidence cites
|
`WARDEN-WP-0032-T05` has landed; whether any compliance evidence cites
|
||||||
`ADR-0004` as implemented.
|
`ADR-0004` as implemented.
|
||||||
|
|
||||||
## Check — 2026-08-20: mitigated on one layer, `high` → `medium`
|
|
||||||
|
|
||||||
`ops-warden` reports all 17 high-risk lanes now exiting 7 on
|
|
||||||
`warden access --fetch` with `WARDEN_AGENT_ID` set, verified under
|
|
||||||
`WARDEN-WP-0032-T05`/`T06`. The 14 ungraded lanes are graded; the boundary
|
|
||||||
fires. The live gap this finding described is closed on the CLI layer.
|
|
||||||
|
|
||||||
**Not clean, so the cadence stays at `instant`.** Something moved, and under
|
|
||||||
the ladder that resets the clock rather than earning a longer one. The next
|
|
||||||
check is the one that can start climbing.
|
|
||||||
|
|
||||||
**`high` → `medium`.** What remains is the omission shape rather than the
|
|
||||||
instance: whether a future lane can still be added without a grade. That is
|
|
||||||
`WARDEN-WP-0032-T06`, and the durable form of the operator's maturity-context
|
|
||||||
rule is `zone-engine`'s (`RISK-N-0004`). Not `low`, because "graded by hand
|
|
||||||
once" is not the same as "cannot be ungraded again".
|
|
||||||
|
|
||||||
**The embargo condition changed rather than being met.** The original condition
|
|
||||||
— the five `exec_capable` lanes graded — is satisfied. But `RISK-F-0009` shows
|
|
||||||
the *second* layer, the OpenBao deny set that protects the direct `bao kv get`
|
|
||||||
path, covers 6 of 17 high-risk lanes. Publishing this finding now would name
|
|
||||||
lanes that are still reachable by the path this control exists to close. The
|
|
||||||
condition is therefore re-pointed at `RISK-F-0009`.
|
|
||||||
|
|
||||||
That is the disclosure rule working across two findings rather than one: what
|
|
||||||
matters is whether the text shortens a path, not whether this particular
|
|
||||||
finding's own fix has landed.
|
|
||||||
|
|
||||||
**Verification limit, recorded rather than assumed:** the mitigation is
|
|
||||||
`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.
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
---
|
---
|
||||||
id: RISK-F-0009
|
id: RISK-F-0004
|
||||||
type: finding
|
type: finding
|
||||||
title: "agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest"
|
title: "agent-high-risk-boundary denies 6 of 17 high-risk lanes; the direct bao path is unprotected for the rest"
|
||||||
status: open
|
status: open
|
||||||
|
|
@ -11,29 +11,12 @@ system: railiance-platform
|
||||||
environment: production
|
environment: production
|
||||||
fix_owner: railiance-platform
|
fix_owner: railiance-platform
|
||||||
fix_tracking: unset
|
fix_tracking: unset
|
||||||
filed_as: "RISK-F-0004 by ops-warden; renumbered by risk-nexus 2026-08-20 (id collision)"
|
severity: unset
|
||||||
answers: RISK-F-0003
|
disclosure: unset
|
||||||
related: [RISK-F-0003]
|
escalation: unset
|
||||||
# Graded by risk-nexus 2026-08-20 — docs/rulings/2026-08-20-openbao-layer.md
|
|
||||||
severity: high
|
|
||||||
severity_at_production: high
|
|
||||||
impact: I4
|
|
||||||
likelihood: L2
|
|
||||||
fidelity_modifier: false
|
|
||||||
production_rescore: false
|
|
||||||
disclosure: embargoed
|
|
||||||
embargo_condition: "the deny set covers every high-risk lane with a KV path, verified against live OpenBao"
|
|
||||||
embargo_since: "2026-08-20"
|
|
||||||
escalation: none
|
|
||||||
last_checked: "2026-08-20T05:40:00Z"
|
|
||||||
next_check: "2026-08-20T05:40:00Z"
|
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
|
||||||
ruling: RISK-RULING-2026-08-20
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# RISK-F-0009 — the OpenBao half of the agent read-boundary covers a third of the lanes
|
# RISK-F-0004 — the OpenBao half of the agent read-boundary covers a third of the lanes
|
||||||
|
|
||||||
## What is true
|
## What is true
|
||||||
|
|
||||||
|
|
@ -118,62 +101,3 @@ this from an audit into an invariant.
|
||||||
- ops-warden `ADR-0004`, `ADR-0007`
|
- ops-warden `ADR-0004`, `ADR-0007`
|
||||||
- `ops-warden/wiki/playbooks/agent-read-boundary.md`
|
- `ops-warden/wiki/playbooks/agent-read-boundary.md`
|
||||||
- `railiance-platform/openbao/policies/agent-high-risk-boundary.hcl`
|
- `railiance-platform/openbao/policies/agent-high-risk-boundary.hcl`
|
||||||
|
|
||||||
## Register ruling — 2026-08-20
|
|
||||||
|
|
||||||
**Renumbered from `RISK-F-0004`.** `ops-warden` filed it as `RISK-F-0004`,
|
|
||||||
which this register had already assigned to `tenant-engine`'s unfiltered event
|
|
||||||
read hours earlier. Earlier id keeps it; the newcomer moves. Nothing about the
|
|
||||||
finding changes, and the filing contract now says how ids are allocated so the
|
|
||||||
next reporter does not hit this.
|
|
||||||
|
|
||||||
`high` (`I4` × `L2`), embargoed, no escalation.
|
|
||||||
|
|
||||||
**`I4` on the worst uncovered lane.** Eight high-risk lanes have a concrete KV
|
|
||||||
path the policy does not deny. Most are `I3`-shaped inside their own system;
|
|
||||||
`scaleway-bootstrap` and `agent-harness-forgejo-deploy` are not — cloud account
|
|
||||||
bootstrap credentials and a forge deploy key propagate past the system that
|
|
||||||
holds them.
|
|
||||||
|
|
||||||
**`L2`.** It needs an agent token carrying a `workload-kv-read-*` policy, which
|
|
||||||
the estate does grant. The register holds `ops-warden`'s two stated limits
|
|
||||||
rather than resolving them in either direction: the comparison is static against
|
|
||||||
the policy *file* because their operator token is expired (`bao token lookup` →
|
|
||||||
403), so **the deployed policy may differ**; and whether any agent token
|
|
||||||
currently carries `agent-high-risk-boundary` together with a
|
|
||||||
`workload-kv-read-*` policy is not established.
|
|
||||||
|
|
||||||
**No fidelity modifier, same call as `RISK-F-0003`.** `ADR-0004` describes two
|
|
||||||
independent layers and one of them covers a third of the lanes, so the
|
|
||||||
documentation overstates the control — but the modifier is scoped to
|
|
||||||
attestations produced at runtime, not to documentation drift. Noted twice now
|
|
||||||
in the same system; if it recurs a third time the modifier's scope is what
|
|
||||||
should be re-examined, not the finding.
|
|
||||||
|
|
||||||
**No escalation.** `railiance-platform` owns the policy, `ops-warden` owns the
|
|
||||||
catalog, and they have not disagreed about anything — trigger 4 needs a failed
|
|
||||||
routing exchange, not merely two repos. `ops-warden`'s suggestion that the deny
|
|
||||||
set be generated from the catalog rather than maintained twice is routed as a
|
|
||||||
suggestion, which is what it was offered as.
|
|
||||||
|
|
||||||
**The pre-existing point is the important one.** Four of the eight uncovered
|
|
||||||
lanes were already graded `high` before the 2026-08-19 regrade. The divergence
|
|
||||||
is not a consequence of that work; the regrade only made the two lists
|
|
||||||
comparable enough to see it. A register that took credit for finding a fresh
|
|
||||||
defect here would be misreading its own evidence.
|
|
||||||
|
|
||||||
**Disclosure: embargoed**, and it changes `RISK-F-0003`'s embargo too. This
|
|
||||||
names eight live KV paths that nothing denies.
|
|
||||||
|
|
||||||
**One expired credential is blocking verification** of both this finding and
|
|
||||||
`RISK-F-0003`'s mitigation. That is not a finding — nothing is wrong with the
|
|
||||||
estate because a token expired — but it is why two gradings here rest on file
|
|
||||||
comparison rather than a probe, and the register says so rather than implying
|
|
||||||
otherwise.
|
|
||||||
|
|
||||||
## Reviews
|
|
||||||
|
|
||||||
- **2026-08-20** — renumbered, graded. Cadence starts at `instant`: nothing
|
|
||||||
about this has been checked twice yet. Open at the next check: does the
|
|
||||||
deployed policy match the file; do any agent tokens carry both policies; has
|
|
||||||
`railiance-platform` taken the catalog-generated deny set.
|
|
||||||
|
|
@ -25,10 +25,8 @@ embargo_condition: "the read path filters by tenant in code"
|
||||||
embargo_since: "2026-08-19"
|
embargo_since: "2026-08-19"
|
||||||
embargo_review: "2026-09-18"
|
embargo_review: "2026-09-18"
|
||||||
escalation: none
|
escalation: none
|
||||||
last_checked: "2026-08-19T21:20:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-19T21:20:00Z" # due now: the ladder starts at instant
|
review_by: "2026-09-18"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19-B
|
ruling: RISK-RULING-2026-08-19-B
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,8 @@ embargo_condition: "AUDIT-WP-0008-T04 lands a tenant filter in the read path"
|
||||||
embargo_since: "2026-08-19"
|
embargo_since: "2026-08-19"
|
||||||
embargo_review: "2026-11-17"
|
embargo_review: "2026-11-17"
|
||||||
escalation: none
|
escalation: none
|
||||||
last_checked: "2026-08-19T21:20:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-19T21:20:00Z" # due now: the ladder starts at instant
|
review_by: "2026-11-17"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19-B
|
ruling: RISK-RULING-2026-08-19-B
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -31,10 +31,8 @@ escalation_answered: "2026-08-19"
|
||||||
escalation_answered_by: the-custodian
|
escalation_answered_by: the-custodian
|
||||||
escalation_act: approve
|
escalation_act: approve
|
||||||
decision: "spend for apps-pg backup storage approved; no ceiling stated"
|
decision: "spend for apps-pg backup storage approved; no ceiling stated"
|
||||||
last_checked: "2026-08-19T23:05:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-19T23:05:00Z" # due now: the ladder starts at instant
|
review_by: "2026-09-18"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19-B
|
ruling: RISK-RULING-2026-08-19-B
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -34,10 +34,8 @@ accepted_by: the-custodian
|
||||||
accepted_on: "2026-08-19"
|
accepted_on: "2026-08-19"
|
||||||
accepted_until: "production transition (hard expiry, not a date)"
|
accepted_until: "production transition (hard expiry, not a date)"
|
||||||
decision: "pragmatic default before production — carried unverified; verification of a named consumer boundary on request"
|
decision: "pragmatic default before production — carried unverified; verification of a named consumer boundary on request"
|
||||||
last_checked: "2026-08-19T23:05:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-19T23:05:00Z" # due now: the ladder starts at instant
|
review_by: "2026-09-18"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19-B
|
ruling: RISK-RULING-2026-08-19-B
|
||||||
---
|
---
|
||||||
|
|
|
||||||
|
|
@ -32,10 +32,8 @@ escalation_act: rule
|
||||||
decision: "identity in audit records: opaque subject ids preferred, agent identifiers where possible, operator credentials only where necessary, policy decisions tracked to the responsible party; zone-level privacy guarantees may raise the floor"
|
decision: "identity in audit records: opaque subject ids preferred, agent identifiers where possible, operator credentials only where necessary, policy decisions tracked to the responsible party; zone-level privacy guarantees may raise the floor"
|
||||||
outstanding: "a defensible retention period per category (waits on audit-core's co-residency horizon), and the trigger list for buying an external answer"
|
outstanding: "a defensible retention period per category (waits on audit-core's co-residency horizon), and the trigger list for buying an external answer"
|
||||||
determination: RISK-REG-0001
|
determination: RISK-REG-0001
|
||||||
last_checked: "2026-08-20T05:25:00Z"
|
last_reviewed: "2026-08-19"
|
||||||
next_check: "2026-08-20T05:25:00Z" # due now: the ladder starts at instant
|
review_by: "2026-11-17"
|
||||||
cadence: instant
|
|
||||||
clean_streak: 0
|
|
||||||
graded_by: risk-nexus
|
graded_by: risk-nexus
|
||||||
ruling: RISK-RULING-2026-08-19-C
|
ruling: RISK-RULING-2026-08-19-C
|
||||||
---
|
---
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -11,11 +11,21 @@ import datetime as dt
|
||||||
|
|
||||||
import register_lib as lib
|
import register_lib as lib
|
||||||
|
|
||||||
NOW = lib.now()
|
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:
|
def main() -> int:
|
||||||
fs = [f for f in lib.findings() if lib.watched(f.get("status"))]
|
# "accepted" is deliberately carried, not closed: it keeps its review
|
||||||
|
# interval and its production re-score, so the nag must keep watching it.
|
||||||
|
fs = [f for f in lib.findings() if f.get("status") in ("open", "accepted")]
|
||||||
lines: list[str] = []
|
lines: list[str] = []
|
||||||
|
|
||||||
def section(title: str, rows: list[str], quiet: str) -> None:
|
def section(title: str, rows: list[str], quiet: str) -> None:
|
||||||
|
|
@ -26,67 +36,44 @@ def main() -> int:
|
||||||
lines.append(f" {quiet}")
|
lines.append(f" {quiet}")
|
||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
unknown = [
|
|
||||||
f"{f['id']} — status '{f.get('status')}' is not one of {', '.join(lib.KNOWN_STATUSES)}; watched anyway"
|
|
||||||
for f in fs
|
|
||||||
if f.get("status") not in lib.KNOWN_STATUSES
|
|
||||||
]
|
|
||||||
if unknown:
|
|
||||||
section("Unrecognised status", unknown, "none")
|
|
||||||
|
|
||||||
ungraded = [
|
ungraded = [
|
||||||
f"{f['id']} — {', '.join(k for k in lib.GRADED_FIELDS if str(f.get(k, 'unset')) == 'unset')}"
|
f"{f['id']} — {', '.join(k for k in lib.GRADED_FIELDS if str(f.get(k, 'unset')) == 'unset')}"
|
||||||
for f in fs
|
for f in fs
|
||||||
if any(str(f.get(k, "unset")) == "unset" for k in lib.GRADED_FIELDS)
|
if any(str(f.get(k, "unset")) == "unset" for k in lib.GRADED_FIELDS)
|
||||||
]
|
]
|
||||||
section("Ungraded", ungraded, "none — every watched finding carries a grade")
|
section("Ungraded", ungraded, "none — every open finding carries a grade")
|
||||||
|
|
||||||
due, deferred = [], []
|
overdue = []
|
||||||
for f in fs:
|
for f in fs:
|
||||||
if f.get("deferred_to"):
|
due = date(f.get("review_by"))
|
||||||
until = lib.moment(f["deferred_to"])
|
if due and TODAY > due:
|
||||||
if until and NOW < until:
|
overdue.append(f"{f['id']} — review was due {due} ({(TODAY - due).days}d ago)")
|
||||||
deferred.append(f"{f['id']} — deferred by the operator until {f['deferred_to']}")
|
section("Overdue review", overdue, "none")
|
||||||
continue
|
|
||||||
when = lib.moment(f.get("next_check"))
|
stale = []
|
||||||
if when is None:
|
for f in fs:
|
||||||
due.append(f"{f['id']} — no next_check set")
|
seen = date(f.get("last_reviewed"))
|
||||||
elif NOW >= when:
|
interval = lib.REVIEW_INTERVAL_DAYS.get(f.get("severity"))
|
||||||
late = NOW - when
|
if not seen or not interval or f.get("severity") == "low":
|
||||||
hours = int(late.total_seconds() // 3600)
|
continue
|
||||||
rung = f.get("cadence", "instant")
|
limit = seen + dt.timedelta(days=interval * lib.STALE_MULTIPLIER)
|
||||||
due.append(
|
if TODAY > limit:
|
||||||
f"{f['id']} ({f.get('severity')}) — due {f['next_check']}"
|
stale.append(
|
||||||
f"{f', {hours}h late' if hours else ''}, cadence {rung}"
|
f"{f['id']} ({f.get('severity')}) — untouched since {seen}; "
|
||||||
|
f"escalation trigger 5 fires (limit was {limit})"
|
||||||
)
|
)
|
||||||
section("Checks due", due, "none")
|
section("Stalled — escalation trigger 5", stale, "none")
|
||||||
if deferred:
|
|
||||||
section("Deferred by the operator", deferred, "none")
|
|
||||||
|
|
||||||
# The rung is the signal: how long this matter has held still.
|
embargo = []
|
||||||
stability = []
|
for f in fs:
|
||||||
for f in sorted(fs, key=lambda f: lib.rung_index(f.get("cadence", "instant"))):
|
if f.get("disclosure") != "embargoed":
|
||||||
rung = f.get("cadence", "instant")
|
continue
|
||||||
streak = f.get("clean_streak", 0)
|
due = date(f.get("embargo_review"))
|
||||||
note = " — at the ceiling" if rung == lib.TOP_RUNG else ""
|
if due and TODAY > due:
|
||||||
stability.append(f"{f['id']}: {rung} ({streak} clean in a row){note}")
|
embargo.append(
|
||||||
section("Stability — where each finding sits on the ladder", stability, "nothing watched")
|
f"{f['id']} — embargo not re-decided since {due}; lifts when: {f.get('embargo_condition')}"
|
||||||
|
)
|
||||||
bottom = [
|
section("Embargo overdue for re-decision", embargo, "none")
|
||||||
f"{f['id']} ({f.get('severity')}) — still at the bottom rung since {f.get('last_checked')}"
|
|
||||||
for f in fs
|
|
||||||
if lib.rung_index(f.get("cadence", "instant")) == 0
|
|
||||||
and (m := lib.moment(f.get("last_checked")))
|
|
||||||
and NOW - m > dt.timedelta(days=14)
|
|
||||||
]
|
|
||||||
section("Stalled — escalation trigger 5", bottom, "none")
|
|
||||||
|
|
||||||
embargo = [
|
|
||||||
f"{f['id']} — lifts when: {f.get('embargo_condition')}"
|
|
||||||
for f in fs
|
|
||||||
if f.get("disclosure") == "embargoed"
|
|
||||||
]
|
|
||||||
section("Embargoed", embargo, "none")
|
|
||||||
|
|
||||||
esc = [
|
esc = [
|
||||||
f"{f['id']} — trigger {f.get('escalation_trigger')}, {f.get('escalation_status')}"
|
f"{f['id']} — trigger {f.get('escalation_trigger')}, {f.get('escalation_status')}"
|
||||||
|
|
@ -102,7 +89,7 @@ def main() -> int:
|
||||||
]
|
]
|
||||||
section("Owed at the production transition", rescore, "none — no finding is graded lower for build mode")
|
section("Owed at the production transition", rescore, "none — no finding is graded lower for build mode")
|
||||||
|
|
||||||
print(f"Register check — {NOW:%Y-%m-%d %H:%MZ}\n{len(fs)} live finding(s)\n")
|
print(f"Register check — {TODAY}\n{len(fs)} open or accepted finding(s)\n")
|
||||||
print("\n".join(lines).rstrip())
|
print("\n".join(lines).rstrip())
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -22,19 +22,14 @@ def date(value) -> dt.date | None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def check_cell(f: dict) -> str:
|
def review_cell(f: dict) -> str:
|
||||||
when = lib.moment(f.get("next_check"))
|
due = date(f.get("review_by"))
|
||||||
if when is None:
|
if due is None:
|
||||||
return "—"
|
return "—"
|
||||||
if lib.now() >= when:
|
late = (TODAY - due).days
|
||||||
return "**due**"
|
if late > 0:
|
||||||
return when.strftime("%Y-%m-%d %H:%MZ")
|
return f"**{due} (overdue {late}d)**"
|
||||||
|
return str(due)
|
||||||
|
|
||||||
def cadence_cell(f: dict) -> str:
|
|
||||||
rung = f.get("cadence", "instant")
|
|
||||||
streak = f.get("clean_streak", 0)
|
|
||||||
return f"{rung} ({streak})"
|
|
||||||
|
|
||||||
|
|
||||||
def sev_cell(f: dict) -> str:
|
def sev_cell(f: dict) -> str:
|
||||||
|
|
@ -55,23 +50,23 @@ def esc_cell(f: dict) -> str:
|
||||||
def render() -> str:
|
def render() -> str:
|
||||||
fs = lib.findings()
|
fs = lib.findings()
|
||||||
ns = lib.notes()
|
ns = lib.notes()
|
||||||
open_fs = [f for f in fs if lib.watched(f.get("status"))]
|
open_fs = [f for f in fs if f.get("status") == "open"]
|
||||||
|
|
||||||
out = [
|
out = [
|
||||||
"# Register",
|
"# Register",
|
||||||
"",
|
"",
|
||||||
f"Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built {TODAY}.",
|
f"Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built {TODAY}.",
|
||||||
"",
|
"",
|
||||||
f"{len(open_fs)} live of {len(fs)} findings; {len(ns)} notes below the floor.",
|
f"{len(open_fs)} open of {len(fs)} findings; {len(ns)} notes below the floor.",
|
||||||
"",
|
"",
|
||||||
"## Findings",
|
"## Findings",
|
||||||
"",
|
"",
|
||||||
"| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Cadence | Next check |",
|
"| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |",
|
||||||
"| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
|
||||||
]
|
]
|
||||||
for f in fs:
|
for f in fs:
|
||||||
out.append(
|
out.append(
|
||||||
"| [{id}]({path}) | {title} | {system} | {sev} | {disc} | {esc} | {owner} | {status} | {cadence} | {check} |".format(
|
"| [{id}]({path}) | {title} | {system} | {sev} | {disc} | {esc} | {owner} | {status} | {review} |".format(
|
||||||
id=f["id"],
|
id=f["id"],
|
||||||
path=f["_path"].relative_to(lib.REPO),
|
path=f["_path"].relative_to(lib.REPO),
|
||||||
title=str(f.get("title", "")).strip('"'),
|
title=str(f.get("title", "")).strip('"'),
|
||||||
|
|
@ -81,8 +76,7 @@ def render() -> str:
|
||||||
esc=esc_cell(f),
|
esc=esc_cell(f),
|
||||||
owner=f.get("fix_owner", "—"),
|
owner=f.get("fix_owner", "—"),
|
||||||
status=f.get("status", "—"),
|
status=f.get("status", "—"),
|
||||||
cadence=cadence_cell(f),
|
review=review_cell(f),
|
||||||
check=check_cell(f),
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -118,9 +112,7 @@ def render() -> str:
|
||||||
|
|
||||||
out += ["", "## How to read this", "",
|
out += ["", "## How to read this", "",
|
||||||
"Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;",
|
"Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;",
|
||||||
"escalation `docs/method/escalation.md`; the check cadence `docs/method/review.md`.",
|
"escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.",
|
||||||
"Cadence is the ladder rung and the count of consecutive clean checks — a finding at `1q (9)`",
|
|
||||||
"has held still for a long time; one at `instant (0)` moved recently. Anything wrong resets it.",
|
|
||||||
"A constraint may be graded higher than the finding that carries it — read both.",
|
"A constraint may be graded higher than the finding that carries it — read both.",
|
||||||
""]
|
""]
|
||||||
return "\n".join(out)
|
return "\n".join(out)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ The finding files are the source of truth. Nothing here writes to them.
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import datetime as dt
|
|
||||||
import pathlib
|
import pathlib
|
||||||
import yaml
|
import yaml
|
||||||
|
|
||||||
|
|
@ -13,53 +12,9 @@ FINDINGS = REPO / "findings"
|
||||||
NOTES = REPO / "notes"
|
NOTES = REPO / "notes"
|
||||||
|
|
||||||
SEVERITIES = ["critical", "high", "medium", "low"]
|
SEVERITIES = ["critical", "high", "medium", "low"]
|
||||||
GRADED_FIELDS = ["severity", "disclosure", "escalation", "next_check", "cadence"]
|
REVIEW_INTERVAL_DAYS = {"critical": 7, "high": 30, "medium": 90, "low": 180}
|
||||||
# A finding leaves the watch list only by being genuinely finished. Anything
|
STALE_MULTIPLIER = 2
|
||||||
# else — including a word the register has not seen before — stays watched, and
|
GRADED_FIELDS = ["severity", "disclosure", "escalation", "review_by", "last_reviewed"]
|
||||||
# the unknown word is reported. A finding must never fall out of the nag because
|
|
||||||
# somebody used a status the tooling did not recognise.
|
|
||||||
CLOSED_STATUSES = ("fixed", "withdrawn")
|
|
||||||
KNOWN_STATUSES = ("open", "accepted", "mitigated") + CLOSED_STATUSES
|
|
||||||
|
|
||||||
|
|
||||||
def watched(status: str | None) -> bool:
|
|
||||||
return status not in CLOSED_STATUSES
|
|
||||||
|
|
||||||
# The adaptive cadence ladder (operator ruling, 2026-08-20). A clean check
|
|
||||||
# climbs one rung; anything wrong drops straight back to `instant`. The rung a
|
|
||||||
# finding sits on is itself the signal: how stable this matter has been.
|
|
||||||
CADENCE = [
|
|
||||||
("instant", dt.timedelta(0)),
|
|
||||||
("1h", dt.timedelta(hours=1)),
|
|
||||||
("8h", dt.timedelta(hours=8)),
|
|
||||||
("24h", dt.timedelta(hours=24)),
|
|
||||||
("48h", dt.timedelta(hours=48)),
|
|
||||||
("96h", dt.timedelta(hours=96)),
|
|
||||||
("7d", dt.timedelta(days=7)),
|
|
||||||
("14d", dt.timedelta(days=14)),
|
|
||||||
("1mo", dt.timedelta(days=30)),
|
|
||||||
("1q", dt.timedelta(days=90)),
|
|
||||||
]
|
|
||||||
CADENCE_NAMES = [name for name, _ in CADENCE]
|
|
||||||
TOP_RUNG = CADENCE_NAMES[-1]
|
|
||||||
|
|
||||||
|
|
||||||
def rung_index(name: str) -> int:
|
|
||||||
return CADENCE_NAMES.index(name) if name in CADENCE_NAMES else 0
|
|
||||||
|
|
||||||
|
|
||||||
def interval(name: str) -> dt.timedelta:
|
|
||||||
return CADENCE[rung_index(name)][1]
|
|
||||||
|
|
||||||
|
|
||||||
def climb(name: str) -> str:
|
|
||||||
"""One clean check: up one rung, never past the quarter."""
|
|
||||||
return CADENCE_NAMES[min(rung_index(name) + 1, len(CADENCE) - 1)]
|
|
||||||
|
|
||||||
|
|
||||||
def reset() -> str:
|
|
||||||
"""Anything wrong: back to the bottom."""
|
|
||||||
return CADENCE_NAMES[0]
|
|
||||||
|
|
||||||
|
|
||||||
def load(path: pathlib.Path) -> dict:
|
def load(path: pathlib.Path) -> dict:
|
||||||
|
|
@ -83,20 +38,5 @@ def notes() -> list[dict]:
|
||||||
return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True)
|
return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True)
|
||||||
|
|
||||||
|
|
||||||
def moment(value) -> dt.datetime | None:
|
|
||||||
"""Parse a date or datetime front-matter value as UTC."""
|
|
||||||
if isinstance(value, dt.datetime):
|
|
||||||
return value if value.tzinfo else value.replace(tzinfo=dt.timezone.utc)
|
|
||||||
if isinstance(value, dt.date):
|
|
||||||
return dt.datetime.combine(value, dt.time(0, 0), tzinfo=dt.timezone.utc)
|
|
||||||
if isinstance(value, str) and value not in ("", "unset"):
|
|
||||||
return dt.datetime.fromisoformat(value.replace("Z", "+00:00")).astimezone(dt.timezone.utc)
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def now() -> dt.datetime:
|
|
||||||
return dt.datetime.now(dt.timezone.utc)
|
|
||||||
|
|
||||||
|
|
||||||
def sev_rank(sev: str) -> int:
|
def sev_rank(sev: str) -> int:
|
||||||
return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)
|
return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue