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

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.