RISK-WP-0001 T01-T06,T08: the four instruments, the index, and the first grading

Severity (impact x likelihood, fidelity modifier for controls that lie,
headline-vs-constraint, build-mode double grade, the floor), disclosure
(publish/embargoed/restricted, and the build-mode deferral re-taken and
narrowed with RISK-F-0001 in hand), escalation (the five INTENT triggers
settled plus an ordering-hazard trigger the RISK-F-0002 case forced;
proposed, awaiting the custodian), review (intervals, what a review is,
what missing one produces, the production re-score).

Then applied: RISK-F-0001 critical/embargoed/escalated, RISK-F-0002
medium with a high constraint on RISK-F-0001's remediation, filed as a
peer and escalated only on the ordering, RISK-F-0003 high/embargoed/no
escalation. No unset field remains.

REGISTER.md is generated; make check reports overdue, stalled, ungraded
and unanswered escalations without changing anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-08-19 23:29:39 +02:00
parent e268259f94
commit d5a3953f2e
15 changed files with 1334 additions and 14 deletions

16
Makefile Normal file
View file

@ -0,0 +1,16 @@
PY := python3
TOOLS := tools
.PHONY: help register check
help:
@echo "make register - rebuild REGISTER.md from findings/"
@echo "make check - verify the index is current, then report what is going quiet"
register:
@$(PY) $(TOOLS)/register_index.py
check:
@$(PY) $(TOOLS)/register_index.py --check
@echo
@$(PY) $(TOOLS)/register_check.py

37
REGISTER.md Normal file
View file

@ -0,0 +1,37 @@
# Register
Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built 2026-08-19.
3 open of 3 findings; 0 notes below the floor.
## Findings
| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
| [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 | open | 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 | **required** (t6, pending-operator) | 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 | **critical** | embargoed | **required** (t1, pending-operator) | flex-auth | open | 2026-08-26 |
## Constraints
Hazards created by acting in the wrong order. Each binds another finding's remediation.
| From | Binds | Severity | Constraint |
| --- | --- | --- | --- |
| RISK-F-0002 | RISK-F-0001 | **high** | policy.enabled must not be turned on while flex-auth /v1/check answers unauthenticated callers — the gate would sign a false attestation |
## Embargoes
Held from publication with a stated condition. A hold with no moving condition is a stall.
| Finding | Since | Lifts when | Re-decided |
| --- | --- | --- | --- |
| 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-0001 | 2026-08-19 | FLEX-WP-0015-T02 ships to production | 2026-08-26 |
## How to read this
Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;
escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.
A constraint may be graded higher than the finding that carries it — read both.

116
docs/method/disclosure.md Normal file
View file

@ -0,0 +1,116 @@
---
id: RISK-METHOD-DISCLOSURE
type: method
title: "Disclosure: publish now, hold, or restrict"
status: adopted
owner: risk-nexus
adopted: "2026-08-19"
workplan: RISK-WP-0001-T02
review_interval: 180d
---
# Disclosure
Whether and when a finding is published. `policy-nexus` is the surface; this
document decides what it is handed.
## What disclosure is not
**A finding file in this repo is not a publication.** This repo is a private
checkout on a private forge. Holding a finding means not routing it to
`policy-nexus`; it does not mean hiding it from the estate, from the owning
repo, or from the operator. Every state below is fully visible internally.
## The states
| State | Meaning | Entry condition |
| --- | --- | --- |
| `public` | Published through `policy-nexus` now. | The finding is fixed, or reading it gives no one an advantage they do not already have. |
| `embargoed` | Held, with a stated condition that lifts it. | Live, unfixed, and the text would help someone reach the defect faster than they could without it. |
| `restricted` | Held with no expected lift. | Publication would remain harmful after the fix — third-party material, a named person, or a credential-shaped detail that survives remediation. |
There is no fourth state and no `unset` after grading. A finding whose
disclosure has not been decided is an ungraded finding.
## Embargo is a record, not a silence
`INTENT.md` requires the record that a delay was deliberate rather than a
document quietly going missing. An `embargoed` finding therefore carries:
```yaml
disclosure: embargoed
embargo_condition: "FLEX-WP-0015-T02 ships to production"
embargo_since: "2026-08-19"
embargo_review: "2026-08-26"
```
`embargo_condition` must be an event someone can observe, not a mood. "Until
it is safer" is not a condition. `embargo_review` follows the finding's
severity interval from `docs/method/review.md`; when it passes, the embargo is
re-decided, not extended by default.
An embargo that has outlived two consecutive reviews without its condition
moving is itself a finding — the remediation has stalled, and the hold is now
doing the work the fix was supposed to do.
## The build-mode deferral, re-taken
`INTENT.md` recorded controlled disclosure as deferred to production, reasoning
that build mode has no users to expose. `RISK-F-0001` arrived and tested it: a
live authorization bypass in the service every other service trusts, with
publish-or-forget as the only available choice.
**The deferral is narrowed, not kept and not abandoned.**
What was right about it: build mode does have no consumers to protect, and
building an embargo *mechanism* — timed release, staged notification,
coordinated disclosure with third parties — before there is anyone to
coordinate with would be machinery for its own sake.
What was wrong about it: it conflated the mechanism with the decision. The
argument for publishing in build mode is that there are no users to expose.
That argument says nothing about attackers, and `RISK-F-0001` is exactly the
class where the two come apart — the finding names an unauthenticated
decision surface and the service that carries it. Publishing that while it is
live helps precisely one kind of reader.
So the ruling is:
1. **Build-mode default stays publish.** Architecture, method, fixed findings,
and findings whose exposure is already bounded go out. The estate publishing
what it knows is wrong remains the norm and does not need a case made for it
each time.
2. **Live-and-reachable is the exception, and it exists now.** A finding that
is unfixed *and* whose text shortens the path to the defect is `embargoed`
until the fix lands. That is the missing middle `INTENT.md` said did not
exist. It costs one front-matter field and a line in `REGISTER.md`.
3. **The mechanism stays deferred.** No timed release, no coordinated
disclosure protocol, no notification tiers. Those wait for real users, as
originally reasoned. What is not deferred is the *decision*, because
`RISK-F-0001` demonstrated the decision is needed before the machinery is.
This is a decision of this repo, taken 2026-08-19 with `RISK-F-0001`,
`RISK-F-0002` and `RISK-F-0003` in hand rather than hypothetically. It is
revisable, and the production transition is the scheduled moment to revisit it.
## Test for "shortens the path"
Ask: does the finding tell a reader something that materially reduces the work
of reaching the defect, beyond what reading the repo would give them?
- A file path and line number in a private repo — no, that is already there.
- "This surface authenticates nobody, here is its cluster address" — yes.
- "These five named lanes vend real secret values without the boundary firing"
— yes.
- "This system had no backups configured" — no, once backups exist; yes, while
they do not, because it names when destruction is unrecoverable.
When the answer is genuinely unclear, embargo and re-decide at the review. The
cost of a wrong embargo is a delayed publication; the cost of a wrong publish
is not recoverable.
## Publication happens elsewhere
A `public` finding is handed to `policy-nexus` under its publication contract
and gets a permanent address there. This repo never serves it and never
edits it after handover; corrections go through the same route as the original.

148
docs/method/escalation.md Normal file
View file

@ -0,0 +1,148 @@
---
id: RISK-METHOD-ESCALATION
type: method
title: "Escalation: what reaches the operator personally"
status: proposed
owner: the-custodian
drafted_by: risk-nexus
drafted: "2026-08-19"
workplan: RISK-WP-0001-T03
review_interval: 90d
---
# Escalation
`INTENT.md` gives `the-custodian` the duty of deciding what must reach the
operator personally rather than sitting in a register, and says the rule is not
yet written. This is the rule, drafted by `risk-nexus`.
**Status: `proposed`.** The custodian adopts it. It is not in force until the
front-matter says `adopted`, and an unadopted rule is worse than an unwritten
one because it looks like coverage.
Getting this wrong in either direction is a failure: escalating everything
makes the operator the queue, escalating nothing makes the register a place
where serious things go quiet.
## What escalation is
A message to the operator personally — inbox message to `the-custodian`, and
said out loud in whatever session is running — containing exactly four things:
1. the finding id and one sentence of what is true;
2. the decision being asked for;
3. what happens if no answer comes;
4. by when.
It is **not** a copy of the finding, not a status report, and not a request for
the operator to do the work. If the operator's answer would not change
anything, it was not an escalation.
The operator is expected to rule, or to defer explicitly with a date. Silence
is not resolution: an unanswered escalation past its date goes back into the
register as an open item on this repo, visible in `REGISTER.md`, and is raised
again once. It is never quietly dropped, and it is never repeated weekly until
someone answers to make it stop.
## The triggers
Each of the five candidates from `INTENT.md` is adopted, dropped, or bounded.
A trigger without a threshold is not a rule.
### 1. Real tenant data — adopted, bounded
Escalate when a finding exposes real data belonging to a real counterparty, or
**the decision that governs access to it**, while live and unfixed.
The bound matters: `INTENT.md` asks whether governing the access counts as
exposing the data. It does. An authorization oracle that can be forged is not
one step removed from the data; it is the step. Test data and empty tenants do
not trigger it — the word is *real*.
### 2. Legal or regulatory obligation — adopted, unbounded
Escalate when a finding creates or reveals an obligation with an outside
counterparty: a notification duty, a retention or erasure requirement, a
contractual commitment, an in-scope determination under a regime.
No threshold. The estate cannot decide unilaterally that an obligation is
small, and this repo is explicitly not the place where legal advice is given.
### 3. Spend — adopted, bounded
Escalate when remediation needs money that is not already committed:
**recurring above €50/month, or one-off above €500.**
Below those, the owning repo decides and records it. The numbers are a
starting proposal from this repo; the operator owns them and should overwrite
them on adoption if they are wrong.
### 4. Ownership disagreement — adopted, bounded by one exchange
Escalate when two repos disagree about who owns a fix **and one routing
exchange has already failed to settle it.**
Disagreement itself is normal and is the register's job to route. What needs
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
three, and not zero.
### 5. Stalled remediation — adopted, bounded by interval
Escalate when a finding's fix has not moved for **twice its review interval**
(`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.
"Not moved" means the fix owner's tracking record has not changed state and no
new evidence has arrived. A deliberate, stated deferral by the owner is
movement. Silence is not.
### 6. Ordering hazard across repos — added
Escalate once when remediating finding A in the wrong order relative to
finding B would create a **false attestation** — a control that starts
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
Stated so the rule bites in both directions.
- **Known, owned, and moving.** However bad. A `critical` finding with an
active fix and a responsive owner is register work.
- **Severity alone.** There is deliberately no "escalate everything
`critical`" trigger. Severity drives review cadence, not the operator's
attention. `RISK-F-0002` argued its own case for not escalating and was
right on triggers 1-5; it reaches the operator only through trigger 6, and
only once.
- **The reporter asking for it.** A repo can flag that it thinks something
needs the operator. That is input, not a decision. This repo rules.
- **Anything this repo can decide itself.** Severity, disclosure, filing
shape, review dates. Asking the operator to confirm a judgement that is this
repo's to make is how the operator becomes the queue.
## Testing the rule against the register
The draft was tested against all three open findings before it was proposed;
the rulings are in `docs/rulings/2026-08-19-first-grading.md`.
Summary: `RISK-F-0001` escalates on trigger 1. `RISK-F-0002` does not escalate
on triggers 1-5 — agreeing with the reporter's own reading — but the
`RISK-F-0001`/`RISK-F-0002` ordering constraint escalates once on trigger 6,
which is the disagreement the rule was worth writing to surface. `RISK-F-0003`
does not escalate; it is known, owned, tracked, and moving.
One escalation, covering two findings, out of three findings. That ratio is
the rule doing its job. If it ever reaches "most findings", the rule is wrong,
not the register.

109
docs/method/review.md Normal file
View file

@ -0,0 +1,109 @@
---
id: RISK-METHOD-REVIEW
type: method
title: "Review and expiry: what happens when nobody looks"
status: adopted
owner: risk-nexus
adopted: "2026-08-19"
workplan: RISK-WP-0001-T04
review_interval: 180d
---
# Review and expiry
`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.
## Intervals
Set from the headline severity at grading time, and reset at each review.
| Severity | Interval | Stale (2×, escalation trigger 5) |
| --- | --- | --- |
| `critical` | 7 days | 14 days |
| `high` | 30 days | 60 days |
| `medium` | 90 days | 180 days |
| `low` | 180 days | — (ages out through review) |
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
to let the date slide.
## What a review is
Four questions, answered in writing on the finding. It takes minutes; it is not
an investigation.
1. **Is the grade still right?** Re-read impact and likelihood against what has
changed. New facts move the grade in both directions.
2. **Is the blocker still true?** This is the one `RISK-F-0002` bought with
evidence: *a blocker is a claim about the world at a date*. Its own stated
blocker — "`flex-auth` is not deployed yet" — was invalidated within a day
by `RISK-F-0001`, and nothing would have re-checked it. Every review
re-checks every stated blocker.
3. **Has the fix moved?** Read the owner's tracking record, not our memory of
it. Confirm the record still exists and still refers to this defect.
4. **Is the disclosure state still right?** An embargo whose condition has
been met is published; an embargo whose condition has not moved in two
reviews is a stall.
The finding gets a dated line under `## Reviews`, `last_reviewed` is updated,
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
observed rather than accidental.
## When a review is missed
Overdue is not a status change on the finding. It is a fact about *this repo*,
and it surfaces in three places:
- `make check` reports it (`tools/register_check.py`), always, unconditionally.
- `REGISTER.md` shows the finding as overdue with the day count.
- At **twice** the interval, escalation trigger 5 fires for `critical`,
`high` and `medium`. That is the point at which the register's own silence
reaches the operator.
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
make a number look better.
## The production re-score
Every finding carries `severity_at_production` alongside `severity`
(`docs/method/severity.md`). Where the two differ, the finding is flagged
`production_rescore: true`.
On the day any part of the estate declares production readiness, every flagged
finding is re-scored before that declaration completes. This is not a review
date — it is an event, and it fires regardless of where the review dates
happen to sit.
Until then, `make check` lists the flagged findings so the size of that
obligation is visible rather than discovered on the day.
## Front-matter this adds
```yaml
last_reviewed: "2026-08-19"
review_by: "2026-08-26"
production_rescore: true
```
`review_by` is what the nag reads. It is a date, never a duration, so that
nothing has to recompute an interval to know whether a finding is late.
## Closing a finding
A finding leaves `open` for exactly one of:
- `fixed` — the owner's record shows the defect gone, and this repo has read
something concrete rather than been told. Publication follows if the
disclosure state was `embargoed`.
- `accepted` — the estate is deliberately carrying it. Requires who accepted
it, why, and a re-review date. `accepted` is not `closed`; it keeps a review
interval forever.
- `withdrawn` — the finding was wrong, or the defect never existed. Say which.
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,
because that is the true state.

169
docs/method/severity.md Normal file
View file

@ -0,0 +1,169 @@
---
id: RISK-METHOD-SEVERITY
type: method
title: "Severity: how bad, how likely, and what is not a risk at all"
status: adopted
owner: risk-nexus
adopted: "2026-08-19"
workplan: RISK-WP-0001-T01
review_interval: 180d
---
# Severity
This is `risk-nexus`'s judgement instrument. It is not canon, it is not a
standard, and it binds nobody else. It exists so that two findings graded a
month apart are graded the same way, and so that a grade can be argued with.
It was written against three real findings (`RISK-F-0001`, `RISK-F-0002`,
`RISK-F-0003`) and it must keep grading those three sensibly or it is wrong.
## The two axes
### Impact — what happens if it goes wrong once
| Band | Name | Test |
| --- | --- | --- |
| `I1` | negligible | Confined to one component. No data leaves it, no record is falsified, no recovery is lost. |
| `I2` | limited | One system's data or availability. Recoverable. Confined to a single tenant, actor or lane. |
| `I3` | serious | Crosses a boundary — tenant, system, or trust — or removes recoverability for one system. |
| `I4` | severe | Crosses the estate. What is compromised here propagates to everything that trusts it, or the data loss is unbounded. |
Impact is scored at **one occurrence**, not at the worst imaginable campaign.
"An attacker who already owns the cluster could do this too" is not an impact
argument.
### Likelihood — how far anyone has to reach
| Band | Name | Test |
| --- | --- | --- |
| `L1` | remote | Requires access nobody currently holds and no ordinary process grants. |
| `L2` | possible | Requires a foothold the estate does grant somewhere — an in-cluster workload, an agent session, a scoped token. |
| `L3` | likely | Reachable from inside the normal working set with no additional step. |
| `L4` | present | No barrier at all, or it is already happening. |
Likelihood is about **reach**, not about intent or about whether anyone has
bothered. `risk-nexus` does not model attackers; it models what the system
permits.
Where the reporter has not established exposure, the finding says so and the
grade uses the band the *stated* facts support — not the worst case, and not
zero. `RISK-F-0001` explicitly declines to assume a default-deny NetworkPolicy
exists; the grade must decline with it, and the unverified fact becomes a
review item rather than a silent assumption in either direction.
## The grid
| | `L1` | `L2` | `L3` | `L4` |
| --- | --- | --- | --- | --- |
| `I4` | medium | high | **critical** | **critical** |
| `I3` | low | medium | high | **critical** |
| `I2` | low | low | medium | high |
| `I1` | note | low | low | medium |
Four severities: `low`, `medium`, `high`, `critical`. `note` is not a
severity; see the floor.
## The fidelity modifier
**A control that lies is one impact band worse than the same control absent.**
Apply `+1` impact band (capped at `I4`) when the failure mode produces a
*false record* rather than *no record*: an attestation that a check passed
when nothing checked, an audit line asserting an authorization that was never
made, a green signal derived from an unreachable test.
The reasoning is `RISK-F-0002`'s and the register adopts it: an absent control
is a gap you can find by looking; a lying control is a gap that survives
looking, because the evidence you would look at is the thing that is wrong.
Only one of the two states misleads the person investigating afterwards.
The modifier applies to the state being scored. A finding that describes both
states — control absent today, control lying if switched on in the wrong order —
gets **two scores and one of them is the register's headline**; see
"Which state is scored".
## Which state is scored
The headline `severity` is the state of the world **today**. A hazard that
would be created by a *future* action is not the headline, because a register
that scores hypotheticals stops describing the estate.
The hazard is not lost. It is recorded on the finding as a named
**constraint** with its own grade, and it attaches to whatever action would
trigger it — usually another finding's remediation. `RISK-F-0002` is the
worked example: the gate being off is today (headline), the gate being
switched on while the oracle is forgeable is a constraint on `RISK-F-0001`'s
fix, graded separately and higher.
If the constraint's grade is higher than the headline, the finding says so in
its ruling. A reader must not be able to come away with the low number and
miss the high one.
## Build mode
Every finding is graded twice:
- `severity` — today, in build mode, with today's likelihood.
- `severity_at_production` — the same impact, with likelihood re-read for a
system carrying real users and real tenant data.
Build mode is a legitimate likelihood input. "No consumer holds real data
here yet" genuinely lowers reach. It is *not* an impact input: what the defect
would do on the day it is exercised does not change because the calendar has
not reached production.
Where the two grades differ, the production transition is a mandatory
re-score. `docs/method/review.md` binds the review date to it, so the re-score
is a scheduled event and not somebody's memory.
## The floor
`INTENT.md`: if a finding would not change anyone's decision, it is a note,
not a risk. Concretely, a register entry requires **both**:
1. **An owner who could act.** Some repo, or the operator, can do something
about it. No actor, no entry.
2. **A decision that changes.** Recording it alters what someone does, when
they do it, or what they must not do first.
Fails either test → it is a note in `notes/`, not a finding in `findings/`.
Notes are not graded, not reviewed, and not published. They exist so that
"we saw it" survives without inflating the register.
An `I1`/`L1` cell is `note` in the grid for the same reason: something that
is both negligible and unreachable is a thing we know, not a risk we carry.
Two things the floor does **not** exclude:
- **Known and deliberate.** `RISK-F-0002` is a decision somebody made on
purpose. It still passes the floor, because it changes what may be switched
on and in what order. Deliberate is not the same as tracked.
- **Omission-shaped.** `RISK-F-0003` is a default that silently produces
ungoverned lanes. The individual lane is small; the default is not.
## Provenance is a grading input
All four defects known to this register were found by repos reading their own
code against a ladder, within days of each other. None was found by monitoring.
Where a finding's provenance is "we happened to look", the register does not
get to assume that similar defects would have been caught. That raises
likelihood for the class, not for the instance, and it belongs in the ruling's
reasoning rather than in a modifier — the register grades what is filed, and
notes when the filing was luck.
## Recording a grade
The finding's front-matter carries:
```yaml
severity: critical # headline, today
severity_at_production: critical
impact: I4 # band, before modifiers
likelihood: L3
fidelity_modifier: false # true if +1 applied, with the reason in the ruling
```
and the ruling section states impact, likelihood, any modifier, and the one
sentence that would have to become false for the grade to change.

View file

@ -0,0 +1,225 @@
---
id: RISK-RULING-2026-08-19
type: ruling
title: "First grading: RISK-F-0001, RISK-F-0002, RISK-F-0003"
status: recorded
owner: risk-nexus
date: "2026-08-19"
workplan: RISK-WP-0001-T06
---
# First grading — 2026-08-19
The first application of `docs/method/`. Three findings, graded together
because the instruments were written for them and a scale that has never been
used is a draft.
Where a grading changed what an owning repo should do, it went back as a
message. No owning repo's files were edited.
## RISK-F-0001 — flex-auth /v1/check authenticates no caller
| Field | Value |
| --- | --- |
| Impact | `I4` |
| Likelihood | `L3` |
| Fidelity modifier | no |
| **Severity** | **critical** |
| At production | critical |
| Disclosure | embargoed |
| Escalation | yes — trigger 1 |
| Review by | 2026-08-26 |
**Impact `I4`.** `flex-auth` is the estate's authorization oracle. A forged
allow does not stay in `flex-auth`; it is the answer other services act on,
and `tenant-engine` has separately stated that direct authority over its rows
means privilege escalation across NetKingdom rather than tampering confined to
one store. That is the definition of `I4` — what is compromised here
propagates to everything that trusts it.
**Likelihood `L3`, not `L4`.** Any workload with cluster reach can assert any
subject and any tenant. That is the normal working set with no additional
step. It is not `L4` only because reach is inside the cluster rather than open.
The register follows the reporter in **not** assuming a default-deny
NetworkPolicy exists, and equally in not assuming its absence proves wider
exposure. `flex-auth` did not state it either way. This is the single fact
that would most change the grade, so it is the first question at review — see
"Asks" below.
**No fidelity modifier.** The endpoint answers honestly about a caller it
never checked; it makes no attestation of its own. The false-record hazard
belongs to `RISK-F-0002`'s constraint, where a *consumer* of this endpoint
would start signing records that assert an authorization was made.
**Production.** Unchanged. Impact is already estate-wide and likelihood does
not depend on there being users.
**Disclosure: embargoed** until `FLEX-WP-0015-T02` ships to production. This
is the finding that forced the deferral to be re-taken
(`docs/method/disclosure.md`): it names an unauthenticated decision surface
and the service carrying it, which shortens the path for exactly one kind of
reader. Publish on fix.
**Escalation: yes, trigger 1.** `INTENT.md` asked whether governing access to
tenant data counts as exposing it. Ruled: it does. An authorization oracle
that can be forged is not one step removed from the data, it is the step.
**Nothing further is asked of `flex-auth`.** They own it, classify it as their
only urgent follow-up, and are shipping through staged promotion.
## RISK-F-0002 — ops-warden signs SSH certificates with no authorization decision
| Field | Value |
| --- | --- |
| Impact | `I3` |
| Likelihood | `L2` |
| Fidelity modifier | no (headline) |
| **Severity** | **medium** |
| Constraint severity | **high** |
| At production | medium |
| Disclosure | embargoed |
| Escalation | via trigger 6 only, jointly with RISK-F-0001 |
| Review by | 2026-11-17 |
`RISK-F-0002` asked this repo three questions. All three are answered here.
### 1. Severity — which state is scored
The headline scores **today**: the gate is off, and that is a missing control
honestly represented. `I3` — SSH certificates into production hosts cross a
trust boundary. `L2` — the caller must already hold a scoped `VAULT_TOKEN` and
the actor must exist in `inventory.yaml`; TTLs are enforced and every issuance
is logged. Possession of the signing token is the whole authorization model,
which is thin, but it is not nothing and it is not `L3`.
**The reporter's argument is accepted in full**: the two states are not equally
bad, and the second is the one worth a severity. It is recorded as a
constraint, not as the headline, because the register describes the estate as
it is and the second state does not exist yet.
> **Constraint (severity `high`).** Enabling `policy.enabled` while
> `flex-auth`'s `/v1/check` answers unauthenticated callers converts an absent
> control into a false attestation: an attacker with ClusterIP reach obtains a
> genuine `allow`, and the signature log carries a `policy_decision_id`
> asserting the issuance was authorized. `I3 + fidelity → I4`, `L2``high`.
The constraint attaches to `RISK-F-0001`'s remediation, and the ordering is
recorded on both findings:
```
flex-auth warn-only -> ops-warden gate presents its SA token -> logs clean
-> flex-auth fail-closed -> ops-warden policy.enabled: true
```
The headline being lower than the constraint is exactly the case
`docs/method/severity.md` was written to handle. A reader must not come away
with "medium" and miss the high one.
### 2. Peer or consequence — **peer**
Filed as its own finding, as `ops-warden` filed it. Two reasons, both from
`INTENT.md`'s routing rule: the fix owner differs, and the "off" state has
standing regardless of how `RISK-F-0001` resolves. If `RISK-F-0001` were
withdrawn tomorrow, `ops-warden` would still be signing certificates with no
per-request judgement.
What is *not* independent is the ordering, and that is carried as a constraint
on both rather than by collapsing one finding into the other.
### 3. Escalation — the register disagrees with the reporter, narrowly
`ops-warden` judged this does not need the operator: known, owned, and its
dangerous failure mode written down in both repos. On triggers 1-5 the
register agrees — no real tenant data, no obligation, no spend, no ownership
dispute, no stall.
But the reasoning "it is written down in both repos" is the argument the
register cannot accept on its own, because `RISK-F-0002` is itself the
evidence against it: its own blocker was written down, filed away, and
invalidated in a day with nothing re-checking it. Written down is not the same
as held.
So **trigger 6 fires once** — an ordering hazard whose wrong sequence creates
a false attestation. One acknowledgement from the operator that the ordering
exists, then the register carries it. Not a standing supervision request.
This is the disagreement the escalation rule was worth writing to surface, and
it is the reason trigger 6 exists at all.
**Disclosure: embargoed** until the ordering completes — `FLEX-WP-0015-T02`
shipped *and* `policy.enabled: true` in production. Publishing "production SSH
signing performs no authorization decision" while true shortens the path.
## RISK-F-0003 — agent read-boundary does not fire on ungraded catalog lanes
| Field | Value |
| --- | --- |
| Impact | `I4` |
| Likelihood | `L2` |
| Fidelity modifier | no (see below) |
| **Severity** | **high** |
| At production | high |
| Disclosure | embargoed |
| Escalation | no |
| Review by | 2026-09-18 |
**Impact `I4`, driven by one lane.** Four of the five `exec_capable` ungraded
lanes are `I3`-shaped — a publish token, an OIDC login, an ingestion key, a
hub write token, each bad within its own system. `openbao-api-key` is not:
it vends access to the store the estate's other secrets live in, and a
disclosure there propagates past every boundary the others respect. The
finding is graded on its worst lane.
**Likelihood `L2`.** Requires an agent session with `WARDEN_AGENT_ID` set —
a foothold the estate does grant. `ops-warden` states the exposed population
is not established and should not be assumed zero or large, and the register
holds that line: `L2` is what the stated facts support. The unverified OpenBao
`agent-high-risk-boundary` coverage would lower it if confirmed, and is a
review question rather than an assumption.
**No fidelity modifier, deliberately, and it was close.** `ADR-0004` reads as
a categorical rule while the implementation is an opt-in list, so the estate's
*documentation* asserts a boundary that does not exist for 14 lanes. That is
documentation drift, and the modifier in `docs/method/severity.md` is scoped
to attestations a system produces at runtime. If any compliance evidence
anywhere cites `ADR-0004` as implemented, that is a false attestation and a
separate finding — this register has not looked, and says so rather than
assuming.
**Escalation: no.** Known, owned, tracked (`WARDEN-WP-0032-T05` for the five
lanes, `WARDEN-WP-0032-T06` for the structural fix), and moving. No trigger
fires. Severity alone deliberately does not escalate.
**The operator's maturity-context direction is noted, not re-decided.** The
finding records that an absent grade should resolve from maturity context —
tolerable in early or experimental contexts, `high` or `critical` in
production ones — and that the durable form waits on the zone model
(`ZONE-WP-0001-T03`). The register's only remark is the one the finding
already makes and this repo confirms from its own registration work today:
`.repo-classification.yaml` `category` cannot carry that signal.
`railiance-platform` runs production OpenBao and owns three of the five lanes
while classified `tooling`, and this repo has just classified itself with the
same vocabulary. The maturity ladder needs its own field.
**Disclosure: embargoed** until the five lanes are graded under
`WARDEN-WP-0032-T05`. It names five live lanes that vend real values without
the boundary firing.
## What this grading asks of other repos
Sent as messages, not as edits.
| Repo | Ask |
| --- | --- |
| `flex-auth` | Confirm whether a default-deny NetworkPolicy fronts the `/v1/check` Service. It is the single fact that most changes a `critical` grade, and only they can answer it. |
| `ops-warden` | Confirm the OpenBao `agent-high-risk-boundary` policy's coverage of the five `exec_capable` lanes. |
| both | The ordering constraint is recorded and carried by this register. Neither repo needs to re-derive it. |
## What this grading proved about the instruments
The scale earned two of its features on first use: the fidelity modifier
(needed by `RISK-F-0002`, and correctly *not* applied to `RISK-F-0003`), and
the headline-versus-constraint split (needed the moment a finding described
two states). The floor was not exercised here — all three findings clear it
easily — and gets its real test in `RISK-WP-0001-T07`.

46
findings/README.md Normal file
View file

@ -0,0 +1,46 @@
# Filing a finding
One file per finding: `findings/RISK-F-NNNN-<slug>.md`, YAML front-matter,
then prose. Next id is one past the highest here.
## What the reporter fills in
```yaml
id: RISK-F-0004
type: finding
title: "one line, what is true — not what should be done"
status: open # open | fixed | accepted | withdrawn
reported_by: <repo> # who found it
reported_via: <repo> # who routed it here, if different
date_reported: "YYYY-MM-DD"
system: <repo> # the system the defect is in
environment: production # production | build | both
fix_owner: <repo> # who owns the fix — never risk-nexus
fix_tracking: <WP-ID or unset>
related: [RISK-F-0001] # optional
```
## What risk-nexus fills in — leave these out
`severity`, `severity_at_production`, `impact`, `likelihood`,
`fidelity_modifier`, `production_rescore`, `disclosure`, `embargo_*`,
`escalation*`, `constraint*`, `last_reviewed`, `review_by`, `graded_by`,
`ruling`.
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
it is and who hears about it (`INTENT.md`). Leaving them out, or writing
`unset`, both work; the nag reports either way until they are graded.
## What makes a good finding here
- **State exposure only as far as you can support it.** "Not established" is a
complete answer and grades better than a guess. `RISK-F-0001` declining to
assume a NetworkPolicy is the model.
- **Say how it was found.** Provenance is a grading input.
- **Suggest a direction if you have one, marked as a suggestion.** The fix is
yours; the grade is ours.
- **A note is fine.** If it would not change anyone's decision, it belongs in
`notes/` — see the floor in `docs/method/severity.md`.
After filing: `make check`. Then this repo grades it.

View file

@ -11,12 +11,24 @@ system: flex-auth
environment: production
fix_owner: flex-auth
fix_tracking: FLEX-WP-0015-T02
# The three fields below are risk-nexus's, not the reporter's. Left unset
# deliberately: the reporter says what is true, this repo says how bad it is
# and who hears about it (INTENT, "What it does not own").
severity: unset
disclosure: unset
escalation: unset
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
severity: critical
severity_at_production: critical
impact: I4
likelihood: L3
fidelity_modifier: false
production_rescore: false
disclosure: embargoed
embargo_condition: "FLEX-WP-0015-T02 ships to production"
embargo_since: "2026-08-19"
embargo_review: "2026-08-26"
escalation: required
escalation_trigger: 1
escalation_status: pending-operator
last_reviewed: "2026-08-19"
review_by: "2026-08-26"
graded_by: risk-nexus
ruling: RISK-RULING-2026-08-19
---
# RISK-F-0001 — flex-auth authenticates no caller on the decision surface
@ -101,3 +113,43 @@ All four were found the same way, by repos reading their own code against a
ladder, within a day of each other. That is a fact about the estate's
observability worth carrying into triage: **four live defects, none found by
monitoring.**
## Register ruling — 2026-08-19
`critical` (`I4` × `L3`, no fidelity modifier), embargoed until
`FLEX-WP-0015-T02` ships, **escalated** to the operator on trigger 1.
The question this finding put — whether governing access to tenant data counts
as exposing it — is answered **yes**. An authorization oracle that can be
forged is not one step removed from the data; it is the step.
Impact is `I4` because a forged allow does not stay here: it is the answer
other services act on, and `tenant-engine` has stated what direct authority
over its rows would mean. Likelihood is `L3` — the normal working set, inside
the cluster, no additional step — and the register follows the reporter in
neither assuming a default-deny NetworkPolicy nor assuming its absence.
No fidelity modifier: this endpoint answers honestly about a caller it never
checked. The false-record hazard lives in `RISK-F-0002`'s constraint, where a
consumer of this endpoint would begin signing records asserting an
authorization that was never made. **That constraint binds this finding's
remediation**: `ops-warden`'s `policy.enabled` must not be turned on until
`/v1/check` authenticates its callers, and the ordering is
```
flex-auth warn-only -> ops-warden gate presents its SA token -> logs clean
-> flex-auth fail-closed -> ops-warden policy.enabled: true
```
Nothing further is asked of `flex-auth` beyond what they have committed to,
except one fact only they can supply: **is there a default-deny NetworkPolicy
in front of the Service?** It is the single fact that would most change this
grade, and it is the first question at review.
Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
## Reviews
- **2026-08-19** — graded. Next review 2026-08-26 (`critical` → 7 days).
Open at review: the NetworkPolicy question; whether `FLEX-WP-0015-T02` has
moved; whether the embargo still holds.

View file

@ -12,10 +12,27 @@ environment: production
fix_owner: ops-warden
fix_tracking: WARDEN-WP-0007 (gate shipped, disabled) / FLEX-WP-0007 (runtime deploy)
related: [RISK-F-0001]
# risk-nexus's to set, not the reporter's (INTENT, "What it does not own").
severity: unset
disclosure: unset
escalation: unset
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
severity: medium
severity_at_production: medium
impact: I3
likelihood: L2
fidelity_modifier: false
production_rescore: false
constraint_on: RISK-F-0001
constraint_severity: high
constraint: "policy.enabled must not be turned on while flex-auth /v1/check answers unauthenticated callers — the gate would sign a false attestation"
disclosure: embargoed
embargo_condition: "FLEX-WP-0015-T02 shipped and ops-warden policy.enabled true in production"
embargo_since: "2026-08-19"
embargo_review: "2026-11-17"
escalation: required
escalation_trigger: 6
escalation_status: pending-operator
last_reviewed: "2026-08-19"
review_by: "2026-11-17"
graded_by: risk-nexus
ruling: RISK-RULING-2026-08-19
---
# RISK-F-0002 — the SSH signing gate is off, and turning it on is now the more dangerous move
@ -120,3 +137,59 @@ the risk of a decision we had already made and filed away as merely blocked.
The estate's habit of recording a blocker once and not revisiting it is the
thing to watch. A blocker is a claim about the world at a date. `RISK-F-0001`
invalidated this one in a day, and nothing would have re-checked it.
## Register ruling — 2026-08-19
All three questions this finding put are answered.
**1. Severity — `medium` today, with a `high` constraint.** The headline
scores the state of the world now: the gate is off, a missing control honestly
represented. `I3` (SSH certificates into production hosts cross a trust
boundary) × `L2` (scoped `VAULT_TOKEN`, actor in `inventory.yaml`, TTLs, every
issuance logged — thin, but not nothing).
The argument that the two states are not equally bad is **accepted in full**,
and it is now written into the scale as the fidelity modifier: a control that
lies is one impact band worse than the same control absent
(`docs/method/severity.md`). It is recorded as a constraint rather than the
headline because the register describes the estate as it is, and the dangerous
state does not exist yet:
> **Constraint, severity `high`.** Enabling `policy.enabled` while `/v1/check`
> answers unauthenticated callers converts an absent control into a false
> attestation — a genuine `allow` obtained by anyone with ClusterIP reach, and
> a `policy_decision_id` in the signature log asserting the issuance was
> authorized. `I3 + fidelity → I4`, `L2``high`.
The constraint is attached to `RISK-F-0001`'s remediation and recorded on both
findings. A reader must not take away `medium` and miss it.
**2. Peer, not consequence.** Filed as `ops-warden` filed it. The fix owner
differs and the "off" state has standing regardless of how `RISK-F-0001`
resolves — if that finding were withdrawn tomorrow, production signing would
still carry no per-request judgement. What is not independent is the ordering,
and that travels as a constraint rather than by collapsing the two records.
**3. Escalation — the register disagrees, narrowly.** On triggers 1-5 it
agrees with `ops-warden`: no real tenant data, no obligation, no spend, no
ownership dispute, no stall. But "it is written down in both repos" is the one
argument the register cannot accept here, because this finding is itself the
evidence against it: its own blocker was written down, filed, and invalidated
in a day with nothing re-checking it.
So trigger 6 — ordering hazard producing a false attestation — fires **once**.
One acknowledgement that the operator holds the ordering, then the register
carries it. Not a standing supervision request. That trigger exists in
`docs/method/escalation.md` because of this finding.
**The general point is adopted.** "A blocker is a claim about the world at a
date" is now question 2 of every review in `docs/method/review.md`, and this
finding is cited there as the case that bought it.
Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
## Reviews
- **2026-08-19** — graded. Next review 2026-11-17 (`medium` → 90 days).
Open at review: has `FLEX-WP-0007` or `WARDEN-WP-0007` moved; is the stated
blocker still true; does the ordering constraint still hold.

View file

@ -11,10 +11,22 @@ system: ops-warden
environment: production
fix_owner: ops-warden
fix_tracking: WARDEN-WP-0032-T05
# risk-nexus's fields, deliberately left unset by the reporter.
severity: unset
disclosure: unset
escalation: unset
# Graded by risk-nexus 2026-08-19 — docs/rulings/2026-08-19-first-grading.md
severity: high
severity_at_production: high
impact: I4
likelihood: L2
fidelity_modifier: false
production_rescore: false
disclosure: embargoed
embargo_condition: "the five exec_capable lanes graded under WARDEN-WP-0032-T05"
embargo_since: "2026-08-19"
embargo_review: "2026-09-18"
escalation: none
last_reviewed: "2026-08-19"
review_by: "2026-09-18"
graded_by: risk-nexus
ruling: RISK-RULING-2026-08-19
---
# RISK-F-0003 — the agent read-boundary has a fourteen-lane blind spot
@ -122,3 +134,50 @@ OpenBao and owns three of the five lanes above, is `category: tooling`.
- `wiki/playbooks/agent-read-boundary.md`
- `RISK-F-0002` — ops-warden sign ungated (same system, different control)
- `zone-engine` `ZONE-WP-0001-T02` — the work that surfaced this
## Register ruling — 2026-08-19
`high` (`I4` × `L2`), embargoed until the five `exec_capable` lanes are graded
under `WARDEN-WP-0032-T05`, no escalation.
**Graded on its worst lane.** Four of the five are `I3`-shaped — a publish
token, an OIDC login, an ingestion key, a hub write token, each bad inside its
own system. `openbao-api-key` is not: it reaches the store the estate's other
secrets live in, and a disclosure there propagates past every boundary the
others respect.
**Likelihood `L2`** — an agent session with `WARDEN_AGENT_ID` is a foothold the
estate does grant. The register holds the line this finding drew: the exposed
population is not established, so it is neither assumed zero nor assumed large,
and the unverified OpenBao `agent-high-risk-boundary` coverage is a review
question rather than an assumption in either direction. Confirming that
coverage for these five paths would lower the grade.
**No fidelity modifier, and it was close.** `ADR-0004` reads categorical while
the implementation is opt-in, so the documentation asserts a boundary that does
not exist for 14 lanes. The modifier is scoped to attestations a system
produces at runtime, and documentation drift is not that. But if any compliance
evidence anywhere cites `ADR-0004` as implemented, that *is* a false
attestation and a separate finding. This register has not looked, and says so
rather than assuming.
**No escalation.** Known, owned, tracked (`WARDEN-WP-0032-T05` for the five
lanes, `WARDEN-WP-0032-T06` for the structural fix), and moving. Severity alone
deliberately does not reach the operator.
**On the maturity-context direction**, which is the operator's and not
re-decided here: the note that `.repo-classification.yaml` `category` cannot
carry the signal is confirmed from this repo's own registration work today.
`railiance-platform` runs production OpenBao and owns three of the five lanes
while classified `tooling`; `risk-nexus` classified itself with the same
vocabulary an hour ago. The maturity ladder needs its own field, and that is
`ZONE-WP-0001-T03`'s to build.
Reasoning: `docs/rulings/2026-08-19-first-grading.md`.
## Reviews
- **2026-08-19** — graded. Next review 2026-09-18 (`high` → 30 days).
Open at review: OpenBao policy coverage of the five paths; whether
`WARDEN-WP-0032-T05` has landed; whether any compliance evidence cites
`ADR-0004` as implemented.

Binary file not shown.

96
tools/register_check.py Normal file
View file

@ -0,0 +1,96 @@
#!/usr/bin/env python3
"""Report what the register is not saying out loud.
RISK-WP-0001-T08. Report only: it writes nothing, changes no field,
escalates nothing on its own, and blocks nothing. A human or the custodian
acts on the output. Exit code is 0 unless a finding cannot be read.
"""
from __future__ import annotations
import datetime as dt
import register_lib as lib
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:
fs = [f for f in lib.findings() if f.get("status") == "open"]
lines: list[str] = []
def section(title: str, rows: list[str], quiet: str) -> None:
lines.append(f"{title}:")
if rows:
lines.extend(f" {r}" for r in rows)
else:
lines.append(f" {quiet}")
lines.append("")
ungraded = [
f"{f['id']}{', '.join(k for k in lib.GRADED_FIELDS if str(f.get(k, 'unset')) == 'unset')}"
for f in fs
if any(str(f.get(k, "unset")) == "unset" for k in lib.GRADED_FIELDS)
]
section("Ungraded", ungraded, "none — every open finding carries a grade")
overdue = []
for f in fs:
due = date(f.get("review_by"))
if due and TODAY > due:
overdue.append(f"{f['id']} — review was due {due} ({(TODAY - due).days}d ago)")
section("Overdue review", overdue, "none")
stale = []
for f in fs:
seen = date(f.get("last_reviewed"))
interval = lib.REVIEW_INTERVAL_DAYS.get(f.get("severity"))
if not seen or not interval or f.get("severity") == "low":
continue
limit = seen + dt.timedelta(days=interval * lib.STALE_MULTIPLIER)
if TODAY > limit:
stale.append(
f"{f['id']} ({f.get('severity')}) — untouched since {seen}; "
f"escalation trigger 5 fires (limit was {limit})"
)
section("Stalled — escalation trigger 5", stale, "none")
embargo = []
for f in fs:
if f.get("disclosure") != "embargoed":
continue
due = date(f.get("embargo_review"))
if due and TODAY > due:
embargo.append(
f"{f['id']} — embargo not re-decided since {due}; lifts when: {f.get('embargo_condition')}"
)
section("Embargo overdue for re-decision", embargo, "none")
esc = [
f"{f['id']} — trigger {f.get('escalation_trigger')}, {f.get('escalation_status')}"
for f in fs
if f.get("escalation") == "required" and f.get("escalation_status") != "answered"
]
section("Escalations awaiting the operator", esc, "none")
rescore = [
f"{f['id']}{f.get('severity')} now, {f.get('severity_at_production')} at production"
for f in fs
if f.get("production_rescore")
]
section("Owed at the production transition", rescore, "none — no finding is graded lower for build mode")
print(f"Register check — {TODAY}\n{len(fs)} open finding(s)\n")
print("\n".join(lines).rstrip())
return 0
if __name__ == "__main__":
raise SystemExit(main())

132
tools/register_index.py Normal file
View file

@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Generate REGISTER.md from the finding front-matter.
RISK-WP-0001-T05. The index is generated because a hand-maintained one
drifts and then lies. Edit the findings, re-run `make register`.
"""
from __future__ import annotations
import datetime as dt
import sys
import register_lib as lib
TODAY = dt.date.today()
def date(value) -> dt.date | None:
if isinstance(value, dt.date):
return value
if isinstance(value, str) and value not in ("", "unset"):
return dt.date.fromisoformat(value)
return None
def review_cell(f: dict) -> str:
due = date(f.get("review_by"))
if due is None:
return ""
late = (TODAY - due).days
if late > 0:
return f"**{due} (overdue {late}d)**"
return str(due)
def sev_cell(f: dict) -> str:
sev = f.get("severity", "unset")
return f"**{sev}**" if sev in ("critical", "high") else sev
def esc_cell(f: dict) -> str:
esc = f.get("escalation", "unset")
if esc in ("none", "unset"):
return esc
trigger = f.get("escalation_trigger")
status = f.get("escalation_status", "")
tail = f" (t{trigger}{', ' + status if status else ''})" if trigger else ""
return f"**{esc}**{tail}"
def render() -> str:
fs = lib.findings()
ns = lib.notes()
open_fs = [f for f in fs if f.get("status") == "open"]
out = [
"# Register",
"",
f"Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. Last built {TODAY}.",
"",
f"{len(open_fs)} open of {len(fs)} findings; {len(ns)} notes below the floor.",
"",
"## Findings",
"",
"| ID | Finding | System | Severity | Disclosure | Escalation | Fix owner | Status | Review by |",
"| --- | --- | --- | --- | --- | --- | --- | --- | --- |",
]
for f in fs:
out.append(
"| [{id}]({path}) | {title} | {system} | {sev} | {disc} | {esc} | {owner} | {status} | {review} |".format(
id=f["id"],
path=f["_path"].relative_to(lib.REPO),
title=str(f.get("title", "")).strip('"'),
system=f.get("system", ""),
sev=sev_cell(f),
disc=f.get("disclosure", "unset"),
esc=esc_cell(f),
owner=f.get("fix_owner", ""),
status=f.get("status", ""),
review=review_cell(f),
)
)
constraints = [f for f in fs if f.get("constraint")]
if constraints:
out += ["", "## Constraints", "",
"Hazards created by acting in the wrong order. Each binds another finding's remediation.",
"", "| From | Binds | Severity | Constraint |", "| --- | --- | --- | --- |"]
for f in constraints:
out.append("| {id} | {on} | **{sev}** | {text} |".format(
id=f["id"], on=f.get("constraint_on", ""),
sev=f.get("constraint_severity", ""), text=f["constraint"]))
embargoed = [f for f in fs if f.get("disclosure") == "embargoed"]
if embargoed:
out += ["", "## Embargoes", "",
"Held from publication with a stated condition. A hold with no moving condition is a stall.",
"", "| Finding | Since | Lifts when | Re-decided |", "| --- | --- | --- | --- |"]
for f in embargoed:
out.append("| {id} | {since} | {cond} | {rev} |".format(
id=f["id"], since=f.get("embargo_since", ""),
cond=f.get("embargo_condition", ""), rev=f.get("embargo_review", "")))
if ns:
out += ["", "## Notes (below the floor)", "",
"Seen, deliberately not findings. Not graded, not reviewed, not published.",
"", "| ID | Note | Why below the floor |", "| --- | --- | --- |"]
for n in ns:
out.append("| [{id}]({path}) | {title} | {why} |".format(
id=n["id"], path=n["_path"].relative_to(lib.REPO),
title=str(n.get("title", "")).strip('"'),
why=str(n.get("floor_reason", "")).strip('"')))
out += ["", "## How to read this", "",
"Severity is `docs/method/severity.md`; disclosure `docs/method/disclosure.md`;",
"escalation `docs/method/escalation.md`; review dates `docs/method/review.md`.",
"A constraint may be graded higher than the finding that carries it — read both.",
""]
return "\n".join(out)
if __name__ == "__main__":
text = render()
target = lib.REPO / "REGISTER.md"
if "--check" in sys.argv:
current = target.read_text(encoding="utf-8") if target.exists() else ""
if current != text:
print("REGISTER.md is stale — run `make register`")
raise SystemExit(1)
print("REGISTER.md is current")
else:
target.write_text(text, encoding="utf-8")
print(f"wrote {target.relative_to(lib.REPO)}")

42
tools/register_lib.py Normal file
View file

@ -0,0 +1,42 @@
"""Shared front-matter reading for the register tools.
The finding files are the source of truth. Nothing here writes to them.
"""
from __future__ import annotations
import pathlib
import yaml
REPO = pathlib.Path(__file__).resolve().parent.parent
FINDINGS = REPO / "findings"
NOTES = REPO / "notes"
SEVERITIES = ["critical", "high", "medium", "low"]
REVIEW_INTERVAL_DAYS = {"critical": 7, "high": 30, "medium": 90, "low": 180}
STALE_MULTIPLIER = 2
GRADED_FIELDS = ["severity", "disclosure", "escalation", "review_by", "last_reviewed"]
def load(path: pathlib.Path) -> dict:
text = path.read_text(encoding="utf-8")
if not text.startswith("---\n"):
raise ValueError(f"{path.name}: no front-matter")
_, fm, _body = text.split("---\n", 2)
data = yaml.safe_load(fm) or {}
data["_path"] = path
return data
def findings() -> list[dict]:
items = [load(p) for p in sorted(FINDINGS.glob("RISK-F-*.md"))]
return sorted(items, key=lambda f: f["id"], reverse=True)
def notes() -> list[dict]:
if not NOTES.exists():
return []
return sorted((load(p) for p in NOTES.glob("RISK-N-*.md")), key=lambda n: n["id"], reverse=True)
def sev_rank(sev: str) -> int:
return SEVERITIES.index(sev) if sev in SEVERITIES else len(SEVERITIES)