Compare commits
2 commits
ba3b3f686d
...
a05ca6822b
| Author | SHA1 | Date | |
|---|---|---|---|
| a05ca6822b | |||
| 449307bea2 |
18 changed files with 399 additions and 28 deletions
6
Makefile
6
Makefile
|
|
@ -1,13 +1,14 @@
|
|||
PY := python3
|
||||
TOOLS := tools
|
||||
|
||||
.PHONY: help register check checked due fixes
|
||||
.PHONY: help register check checked due fixes coverage
|
||||
|
||||
help:
|
||||
@echo "make register - rebuild REGISTER.md from findings/"
|
||||
@echo "make check - verify the index, report what is going quiet, and read the inbox"
|
||||
@echo "make due - just the work list: what needs a check right now"
|
||||
@echo "make fixes - the state of every fix this register claims to track"
|
||||
@echo "make coverage - what the register has never heard from"
|
||||
@echo "make checked - record a check outcome: make checked ARGS=\"RISK-F-0002 clean\""
|
||||
|
||||
register:
|
||||
|
|
@ -30,3 +31,6 @@ due:
|
|||
|
||||
fixes:
|
||||
@$(PY) $(TOOLS)/fix_tracker.py
|
||||
|
||||
coverage:
|
||||
@$(PY) $(TOOLS)/coverage.py
|
||||
|
|
|
|||
|
|
@ -1,7 +1,12 @@
|
|||
# risk-nexus
|
||||
|
||||
Risk register and regulatory intake for the estate. Serves
|
||||
`risk.coulomb.social`. Owned by `the-custodian`.
|
||||
Risk register and regulatory intake for the estate. Owned by `the-custodian`.
|
||||
|
||||
**It does not serve anything yet.** `INTENT.md` names `risk.coulomb.social` as
|
||||
the eventual surface; today publication runs through `policy-nexus` and three
|
||||
documents are waiting for an address. Recorded here rather than left as a
|
||||
claim, because a stated surface that does not exist is the class of thing this
|
||||
register grades other repos down for.
|
||||
|
||||
Holds findings — security, architecture, operational, compliance — with a
|
||||
severity, an owner and a date; decides whether and when each is published; and
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ Generated by `tools/register_index.py` from `findings/`. Do not edit by hand. La
|
|||
| [RISK-F-0006](findings/RISK-F-0006-apps-pg-no-backup-configured.md) | apps-pg has no backup configured at all: R0 means no recovery | railiance-platform | **high** | embargoed | **answered** (t3, approved) | railiance-platform | open | 1h (1) | **due** |
|
||||
| [RISK-F-0005](findings/RISK-F-0005-audit-core-unfiltered-read-path.md) | audit-core read path applies no tenant filter; the bound is deployment, not code | audit-core | medium | public | none | audit-core | mitigated | instant (0) | **due** |
|
||||
| [RISK-F-0004](findings/RISK-F-0004-tenant-engine-unfiltered-event-read.md) | tenant-engine events() returns the entire event log unfiltered | tenant-engine | **high** | embargoed | none | tenant-engine | open | 1h (1) | **due** |
|
||||
| [RISK-F-0003](findings/RISK-F-0003-ops-warden-read-boundary-ungraded-lanes.md) | ops-warden agent read-boundary does not fire on ungraded catalog lanes | ops-warden | medium | embargoed | none | ops-warden | mitigated | 1h (1) | **due** |
|
||||
| [RISK-F-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 | 8h (2) | 2026-08-21 14:32Z |
|
||||
| [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-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** |
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@ Then, per due finding:
|
|||
Then record it, in the same sitting:
|
||||
|
||||
```
|
||||
RISK_CHECKED_BY=<who-or-what-you-are> \
|
||||
make checked ARGS="RISK-F-0002 clean"
|
||||
make checked ARGS="RISK-F-0002 moved 'ops-warden enabled the gate'"
|
||||
make checked ARGS="RISK-F-0002 defer 2026-09-01 'operator: after the migration'"
|
||||
|
|
|
|||
|
|
@ -184,6 +184,26 @@ The test therefore applies to intake, not to sweeps:
|
|||
If that trips, the first suspects are triggers 3 and 4 — spend thresholds set
|
||||
too low, and "unowned" being read where "not yet routed" is meant.
|
||||
|
||||
## Delivery is a state, not an act
|
||||
|
||||
`RISK-WP-0005-T05`. An escalation that nobody acknowledged is indistinguishable
|
||||
from one never sent — which is precisely the failure this register committed
|
||||
on 2026-08-19 and then fixed for its **own** inbox with an hourly watch, while
|
||||
leaving the path that matters more unguarded.
|
||||
|
||||
So an escalation carries a state:
|
||||
|
||||
```yaml
|
||||
escalation_status: pending-operator # sent | seen | answered | withdrawn
|
||||
escalation_sent: "2026-08-19"
|
||||
```
|
||||
|
||||
`make check` reports how long each has been unacknowledged. At seven days it
|
||||
says so and the escalation is **raised once more** — once, per the rule above.
|
||||
After that the default applies and is recorded. Repetition until someone
|
||||
answers is how the operator becomes the queue, and silence that is recorded is
|
||||
not the same as silence that is ignored.
|
||||
|
||||
## Escalations are batched
|
||||
|
||||
Four escalations are one conversation, not four interruptions. Open items go
|
||||
|
|
|
|||
93
docs/method/intake.md
Normal file
93
docs/method/intake.md
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
---
|
||||
id: RISK-METHOD-INTAKE
|
||||
type: method
|
||||
title: "Intake: the four sources, including the two that had no path"
|
||||
status: adopted
|
||||
owner: risk-nexus
|
||||
adopted: "2026-08-21"
|
||||
workplan: RISK-WP-0005-T02
|
||||
review_interval: 6m
|
||||
---
|
||||
|
||||
# Intake
|
||||
|
||||
`INTENT.md` claims intake "from any source: research, review, incident,
|
||||
external report". Until 2026-08-21 two of those four had no path at all, and
|
||||
every finding in the register had arrived from a repo reading its own code.
|
||||
|
||||
A register that only receives self-assessments is a self-assessment aggregator.
|
||||
That is a respectable thing to be and it is not what the intent says.
|
||||
|
||||
## The four sources
|
||||
|
||||
| Source | Path | State |
|
||||
| --- | --- | --- |
|
||||
| Research | a repo or agent files against `findings/README.md` | working, nine findings |
|
||||
| Review | same, usually against a ladder or standard | working, most of the nine |
|
||||
| **Incident** | below | new |
|
||||
| **External report** | below — and the address is not this repo's to create | routed |
|
||||
|
||||
## Incident
|
||||
|
||||
An incident is a finding about something happening **now** rather than a latent
|
||||
defect. The difference is not severity; it is that someone is exercising it, or
|
||||
the loss is occurring, while the record is being written.
|
||||
|
||||
### What is different
|
||||
|
||||
- **File first, grade later.** The normal order is reversed: get the record
|
||||
into `findings/` with what is known, and grade it in the same hour rather
|
||||
than in the same sitting. An ungraded incident is still a record; an unwritten
|
||||
one is nothing.
|
||||
- **`incident: true` and a `first_observed` timestamp**, both in front-matter.
|
||||
The timestamp matters more than the grade, because obligations run from it.
|
||||
- **Cadence starts at `instant` and stays there** until the incident is over.
|
||||
That is the ladder's existing behaviour and needs no special case: something
|
||||
that keeps moving keeps resetting.
|
||||
- **The 72-hour clock.** If personal data is or may be involved,
|
||||
`RISK-POL-0005` (GDPR Arts 33–34) requires notification within 72 hours of
|
||||
becoming aware. **`first_observed` is what starts that clock**, and this is
|
||||
the one place the register puts a hard deadline on itself. Escalation is
|
||||
immediate under trigger 1 or 2 — not batched, notwithstanding the batching
|
||||
rule, because batching exists to protect the operator's attention and a
|
||||
72-hour clock outranks that.
|
||||
- **Severity is scored on what is happening, not what could.** The `L4`
|
||||
likelihood band — "already happening" — exists for exactly this and is
|
||||
otherwise almost unused.
|
||||
|
||||
### What is not different
|
||||
|
||||
Ownership, routing, and the boundary. The repo that owns the system owns the
|
||||
response; this register records, grades, escalates and keeps the clock. A risk
|
||||
register that runs incident response is a second engineering team with a
|
||||
stopwatch.
|
||||
|
||||
## External report
|
||||
|
||||
Someone outside the estate finding something and having somewhere to send it.
|
||||
|
||||
**No such place exists.** There is no `security.txt` anywhere in the estate, no
|
||||
published address, and no route from an outsider to this register. If someone
|
||||
found a flaw in a coulomb.social surface tomorrow, their options would be to
|
||||
guess an email address or say nothing.
|
||||
|
||||
**Where that address lives is not this repo's decision.** It is a published
|
||||
surface, which makes it `policy-nexus`'s to serve and the custodian's to
|
||||
approve. Routed on 2026-08-21 with a proposal:
|
||||
|
||||
- a `security.txt` (RFC 9116) at `policy.coulomb.social/.well-known/`, naming
|
||||
a contact and a preferred language, with an expiry date as the RFC requires;
|
||||
- reports arriving there routed to `risk-nexus` and filed like any other
|
||||
finding, with `reported_by: external`;
|
||||
- **no promise of a bounty, a timeline, or safe harbour.** The ask is a route
|
||||
in, not a commitment out. A published disclosure policy is a later decision
|
||||
and a heavier one.
|
||||
|
||||
Until that exists, this register's claim to accept external reports is
|
||||
aspirational and `STATE.md` says so.
|
||||
|
||||
## What all four share
|
||||
|
||||
The reporter says what is true and how they know. This register says how bad it
|
||||
is and who hears about it. Nothing in this document changes that boundary —
|
||||
including for incidents, where the temptation to take over is strongest.
|
||||
91
docs/method/production-transition.md
Normal file
91
docs/method/production-transition.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
id: RISK-METHOD-PRODUCTION
|
||||
type: method
|
||||
title: "The production transition: what expires, and who says when"
|
||||
status: proposed
|
||||
owner: the-custodian
|
||||
drafted_by: risk-nexus
|
||||
drafted: "2026-08-21"
|
||||
workplan: RISK-WP-0005-T03
|
||||
review_interval: 6m
|
||||
---
|
||||
|
||||
# The production transition
|
||||
|
||||
Five findings owe a re-score here. One acceptance **expires** here. Several
|
||||
dormant legal policies activate here. And until 2026-08-21 nobody had defined
|
||||
the event, named who declares it, or told the repos that carry the obligations.
|
||||
|
||||
An acceptance that expires on an undefined event expires on nobody's calendar.
|
||||
|
||||
**Status: `proposed`.** The definition is the custodian's; the plumbing is
|
||||
this repo's.
|
||||
|
||||
## Proposed definition
|
||||
|
||||
> **The production transition is the first moment any system in the estate
|
||||
> holds, processes or decides about the data of a real external person or
|
||||
> tenant.**
|
||||
|
||||
Deliberately not: a launch announcement, a version number, a deployment to a
|
||||
cluster named production, or a declaration made in advance. All four can be
|
||||
true while nothing real is at stake, and all four can be false on the day
|
||||
something is.
|
||||
|
||||
Two consequences worth stating, because they are what make the definition
|
||||
usable:
|
||||
|
||||
- **It can happen by accident.** A single real signup, a migrated contact list,
|
||||
one counterparty's document sent through a pipeline for testing. Which is
|
||||
why it is defined by what is held rather than by what was announced.
|
||||
- **It is not reversible.** Deleting the data afterwards does not un-transition
|
||||
the estate; it changes the exposure and not the fact that the estate is now
|
||||
the kind of thing that holds real data.
|
||||
|
||||
## Who declares it
|
||||
|
||||
The operator, through `the-custodian`. Not this register: `INTENT.md` keeps
|
||||
"deciding what canon says" and estate-level facts with the custodian, and a
|
||||
risk register that unilaterally declares the estate to be in production has
|
||||
appointed itself.
|
||||
|
||||
What this register does is **notice and ask**. Any finding, verification or
|
||||
policy check that turns up real external data in a system triggers one question
|
||||
to the custodian: *has the transition happened?* The answer is theirs; the
|
||||
record is ours.
|
||||
|
||||
## What fires on the day
|
||||
|
||||
| Obligation | Carried by | Count today |
|
||||
| --- | --- | --- |
|
||||
| Re-score every finding with `production_rescore: true` | this register | 5 |
|
||||
| `RISK-F-0007`'s acceptance expires — the unverified tenant boundary stops being carried | this register, escalating | 1 |
|
||||
| `RISK-POL-0001`, `0003`, `0004`, `0005`, `0008` activate; `0002` fully | owning repos | 6 policies |
|
||||
| `RISK-F-0008`'s acceptance ends — the retention basis stops being an internal determination | this register | 1 |
|
||||
|
||||
`make check` lists the first two under "owed at the production transition"
|
||||
today, so the size of the obligation is visible before the day rather than
|
||||
discovered on it.
|
||||
|
||||
## How the register hears about it
|
||||
|
||||
Three routes, in order of reliability:
|
||||
|
||||
1. **The custodian says so**, in a message. Preferred, and the only one that is
|
||||
also a decision.
|
||||
2. **A finding or verification turns up real external data**, and this register
|
||||
asks. This is the route that works when nobody remembered to declare.
|
||||
3. **A legal policy activates on its own condition** — the accessibility or
|
||||
data-subject-rights entries in `docs/regulatory/policies/` fire on facts
|
||||
about the world, not on a declaration, and a repo consulting one is evidence
|
||||
the transition may have happened.
|
||||
|
||||
Route 2 exists because route 1 is exactly the kind of announcement that gets
|
||||
skipped in a busy week, and the estate has already demonstrated (`RISK-POL-0012`)
|
||||
that an obligation can be live for nineteen months without anyone noticing.
|
||||
|
||||
## What this document does not do
|
||||
|
||||
It does not schedule the transition, gate it, or require anything before it. In
|
||||
build mode a finding is information — that is unchanged. This says only what
|
||||
becomes true on a day that will arrive whether or not it is planned.
|
||||
|
|
@ -49,6 +49,17 @@ Two rules and one escape:
|
|||
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.
|
||||
|
||||
### Who checked, and when anyone last did
|
||||
|
||||
`RISK-WP-0005-T06`. Two defences against the rung telling a lie:
|
||||
|
||||
- **`checked_by` on every check.** `record_check.py` writes it. A rung earned
|
||||
by nobody in particular is visible as such.
|
||||
- **A heartbeat.** If nothing anywhere in the register has been checked for two
|
||||
days, `make check` says so **before anything else**. A `1q` rung means
|
||||
"stable for a quarter" and "nobody looked for a quarter", and those read
|
||||
identically from the outside — the heartbeat is what separates them.
|
||||
|
||||
### The rung is the signal
|
||||
|
||||
This is the point of the design, not a side effect. **The cadence a finding
|
||||
|
|
|
|||
|
|
@ -142,6 +142,19 @@ comes back.
|
|||
> *Added 2026-08-19 (`RISK-WP-0001-T07`).* Forced by `RISK-F-0006`, where the
|
||||
> defect is an absent backup and the reach reading produced nonsense.
|
||||
|
||||
## Live incidents
|
||||
|
||||
Everything above assumes a latent defect — something reachable that nobody is
|
||||
currently reaching. When someone is, three things change:
|
||||
|
||||
- **Likelihood is `L4`.** The band means "already happening" and this is what
|
||||
it is for.
|
||||
- **Impact is scored on what has occurred plus what is still reachable**, not
|
||||
on the worst case. An incident in progress has facts; use them.
|
||||
- **The grade is provisional and expected to move.** File first, grade within
|
||||
the hour, re-grade as facts arrive. `docs/method/intake.md` has the rest,
|
||||
including the 72-hour clock that `first_observed` starts.
|
||||
|
||||
## The floor
|
||||
|
||||
`INTENT.md`: if a finding would not change anyone's decision, it is a note,
|
||||
|
|
|
|||
|
|
@ -27,9 +27,9 @@ publication_subtitle: "The estate's authorization oracle authenticated no caller
|
|||
revision: "graded-1"
|
||||
last_reviewed: "2026-08-20"
|
||||
review_interval: 6m
|
||||
embargo_condition: "met 2026-08-19 — FLEX-WP-0015 finished, live probes return 401"
|
||||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-08-19"
|
||||
embargo_lifted: "2026-08-19 — FLEX-WP-0015 finished, live probes return 401"
|
||||
embargo_was_since: "2026-08-19"
|
||||
|
||||
escalation: withdrawn
|
||||
escalation_trigger: 1
|
||||
escalation_status: withdrawn-before-sending
|
||||
|
|
|
|||
|
|
@ -24,12 +24,13 @@ embargo_condition: "RISK-F-0009 resolved — the OpenBao deny set covers every h
|
|||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-09-18"
|
||||
escalation: none
|
||||
last_checked: "2026-08-20T10:02:42Z"
|
||||
next_check: "2026-08-20T11:02:42Z"
|
||||
cadence: 1h
|
||||
clean_streak: 1
|
||||
last_checked: "2026-08-21T06:32:10Z"
|
||||
next_check: "2026-08-21T14:32:10Z"
|
||||
cadence: 8h
|
||||
clean_streak: 2
|
||||
graded_by: risk-nexus
|
||||
ruling: RISK-RULING-2026-08-19
|
||||
checked_by: "worsch"
|
||||
---
|
||||
|
||||
# RISK-F-0003 — the agent read-boundary has a fourteen-lane blind spot
|
||||
|
|
@ -226,3 +227,4 @@ finding's own fix has landed.
|
|||
`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.
|
||||
- **2026-08-20** — clean check: checked against the inbox and the owner's record; nothing moved. Cadence instant → 1h (1 clean in a row); next check 2026-08-20 11:02Z.
|
||||
- **2026-08-21** — clean check: fix state read from the owner's file: WARDEN-WP-0032-T05 done; nothing else moved. Cadence 1h → 8h (2 clean in a row); next check 2026-08-21 14:32Z.
|
||||
|
|
|
|||
|
|
@ -22,9 +22,9 @@ fidelity_modifier: false
|
|||
production_rescore: true
|
||||
disclosure: public
|
||||
publication: pending-handover
|
||||
embargo_condition: "met 2026-08-18 — AUDIT-WP-0008-T04 reads done in audit-core's workplan"
|
||||
embargo_since: "2026-08-19"
|
||||
embargo_review: "2026-11-17"
|
||||
embargo_lifted: "2026-08-18 — AUDIT-WP-0008-T04 reads done in audit-core's workplan"
|
||||
embargo_was_since: "2026-08-19"
|
||||
|
||||
escalation: none
|
||||
last_checked: "2026-08-21T06:29:38Z"
|
||||
next_check: "2026-08-21T06:29:38Z"
|
||||
|
|
|
|||
|
|
@ -32,3 +32,23 @@ anything nobody thought to look at.
|
|||
It comes back the first time a finding arrives that monitoring plausibly should
|
||||
have caught and did not. That is the evidence this note is missing, and until
|
||||
then filing it would be the register asserting a conclusion it cannot support.
|
||||
|
||||
## Update — 2026-08-21: the denominator
|
||||
|
||||
`RISK-WP-0005-T07` built the smallest possible coverage report, and it puts a
|
||||
number on what this note could previously only gesture at:
|
||||
|
||||
> **7 of 117 registered repos have ever appeared in a finding. 110 never have.**
|
||||
|
||||
That is not 110 clean repos and this note does not claim it is. It is 110 repos
|
||||
about which the register knows nothing, and the estate's own evidence — two for
|
||||
two on tenant boundaries, four defects in one review round — says that looking
|
||||
tends to find something.
|
||||
|
||||
The note stays a note. There is still no owner for "the estate's ability to
|
||||
notice its own defects", and nothing here is a defect to route. What changed is
|
||||
that the gap now has a size, and `make coverage` prints it, which is the
|
||||
difference between an argument and a measurement.
|
||||
|
||||
It comes back as a finding the first time something is found in one of the 110
|
||||
that a reasonable sweep would have caught earlier.
|
||||
|
|
|
|||
BIN
tools/__pycache__/fix_tracker.cpython-312.pyc
Normal file
BIN
tools/__pycache__/fix_tracker.cpython-312.pyc
Normal file
Binary file not shown.
65
tools/coverage.py
Normal file
65
tools/coverage.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env python3
|
||||
"""What has never been looked at.
|
||||
|
||||
RISK-WP-0005-T07, deliberately minimal. The register knows what was reported.
|
||||
Without this it has no view of what was never assessed, so a system with zero
|
||||
findings is indistinguishable from a system nobody has examined — while
|
||||
RISK-N-0003 records that every repo which *has* examined its own boundary this
|
||||
month found a defect.
|
||||
|
||||
This does not assess anything and does not grade anyone. It counts.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
import register_lib as lib
|
||||
|
||||
HUB = "http://127.0.0.1:8000/repos/"
|
||||
|
||||
|
||||
def repos() -> list[dict] | None:
|
||||
for url in (HUB, "http://127.0.0.1:8000/repos"):
|
||||
try:
|
||||
with urllib.request.urlopen(url, timeout=8) as r:
|
||||
data = json.load(r)
|
||||
return data if isinstance(data, list) else data.get("items", [])
|
||||
except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, OSError):
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def main() -> int:
|
||||
all_repos = repos()
|
||||
seen: dict[str, list[str]] = {}
|
||||
for f in lib.findings():
|
||||
for key in (f.get("system"), f.get("fix_owner"), f.get("reported_by")):
|
||||
if key and isinstance(key, str):
|
||||
seen.setdefault(key, []).append(f["id"])
|
||||
|
||||
print("Coverage — what the register has heard from\n")
|
||||
print(f" systems that have produced or carried a finding: {len(seen)}")
|
||||
for name in sorted(seen):
|
||||
print(f" {name}: {', '.join(sorted(set(seen[name])))}")
|
||||
|
||||
if all_repos is None:
|
||||
print("\n Hub unreachable — the denominator is unknown, which is the whole point of this report.")
|
||||
return 0
|
||||
|
||||
names = {r.get("slug") for r in all_repos if r.get("slug")}
|
||||
unheard = sorted(names - set(seen))
|
||||
print(f"\n registered repos: {len(names)}")
|
||||
print(f" never appeared in any finding: {len(unheard)}")
|
||||
print("\n A repo in that list has either nothing wrong with it or nobody looking.")
|
||||
print(" This register cannot tell which, and does not guess.\n")
|
||||
for name in unheard[:40]:
|
||||
print(f" {name}")
|
||||
if len(unheard) > 40:
|
||||
print(f" … and {len(unheard) - 40} more")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
|
|
@ -13,6 +13,7 @@ written down, which is the same rule the register applies to everyone else.
|
|||
from __future__ import annotations
|
||||
|
||||
import datetime as dt
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
|
@ -62,6 +63,7 @@ def main(argv: list[str]) -> int:
|
|||
fail("outcome must be one of: clean, moved, defer")
|
||||
|
||||
subs = {
|
||||
"checked_by": os.environ.get("RISK_CHECKED_BY", os.environ.get("USER", "unknown")),
|
||||
"last_checked": stamp,
|
||||
"next_check": nxt.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"cadence": new_rung,
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ def main() -> int:
|
|||
for ref in (f.get("related") or []):
|
||||
if ref not in ids:
|
||||
malformed.append(f"{fid} — related names {ref}, which does not exist")
|
||||
if f.get("embargo_condition") and f.get("disclosure") != "embargoed":
|
||||
if f.get("embargo_condition") and f.get("disclosure") != "embargoed": # a lift is recorded as embargo_lifted
|
||||
malformed.append(f"{fid} — carries an embargo_condition but disclosure is '{f.get('disclosure')}'")
|
||||
if f.get("disclosure") == "embargoed" and not f.get("embargo_condition"):
|
||||
malformed.append(f"{fid} — embargoed with no condition; a hold with no lift is a silence")
|
||||
|
|
@ -159,11 +159,20 @@ def main() -> int:
|
|||
]
|
||||
section("Embargoed", 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"
|
||||
]
|
||||
# T05 — an escalation nobody acknowledged is indistinguishable from one
|
||||
# never sent, which is the failure this register fixed for its own inbox
|
||||
# and not, until now, for the path that matters more.
|
||||
esc = []
|
||||
for f in fs:
|
||||
if f.get("escalation") != "required" or f.get("escalation_status") == "answered":
|
||||
continue
|
||||
sent = lib.moment(f.get("escalation_sent"))
|
||||
age = f", sent {(NOW - sent).days}d ago" if sent else ", never marked sent"
|
||||
state = f.get("escalation_status", "unknown")
|
||||
tail = " ← unacknowledged; raise once more, then record the default" if (
|
||||
sent and (NOW - sent).days >= 7 and state not in ("seen", "answered")
|
||||
) else ""
|
||||
esc.append(f"{f['id']} — trigger {f.get('escalation_trigger')}, {state}{age}{tail}")
|
||||
section("Escalations awaiting the operator", esc, "none")
|
||||
|
||||
rescore = [
|
||||
|
|
@ -182,6 +191,17 @@ def main() -> int:
|
|||
reg.append(f"{r.get('id')} — due {when:%Y-%m-%d}: {str(r.get('title','')).strip()}")
|
||||
section("Regulatory records due", reg, "none")
|
||||
|
||||
# T06 — a 1q rung means "stable for a quarter" and "nobody looked for a
|
||||
# quarter", and those read identically. The heartbeat separates them.
|
||||
checks = [m for f in lib.findings() if (m := lib.moment(f.get("last_checked")))]
|
||||
if checks:
|
||||
newest = max(checks)
|
||||
quiet = (NOW - newest).days
|
||||
if quiet >= 2:
|
||||
lines.insert(0, "")
|
||||
lines.insert(0, f" Nothing anywhere in the register has been checked for {quiet} days.")
|
||||
lines.insert(0, "HEARTBEAT — THE LADDER IS NOT BEING CLIMBED:")
|
||||
|
||||
print(f"Register check — {NOW:%Y-%m-%d %H:%MZ}\n{len(fs)} live finding(s)\n")
|
||||
print("\n".join(lines).rstrip())
|
||||
return 0
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ type: workplan
|
|||
title: "Close the gaps between what INTENT claims and what the register can actually do"
|
||||
domain: infotech
|
||||
repo: risk-nexus
|
||||
status: active
|
||||
status: finished
|
||||
owner: the-custodian
|
||||
topic_slug: risk-nexus
|
||||
created: "2026-08-21"
|
||||
|
|
@ -45,7 +45,7 @@ plainly at the top:
|
|||
|
||||
```task
|
||||
id: RISK-WP-0005-T01
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
|
|
@ -69,11 +69,17 @@ whether or not the register has been checking.
|
|||
Where `fix_tracking` is `unset` (`RISK-F-0004`, `RISK-F-0006`, `RISK-F-0009`),
|
||||
that absence is itself the report.
|
||||
|
||||
Completed 2026-08-21. `tools/fix_tracker.py`, behind `make fixes` and inside `make check`. Resolves `fix_tracking` against the owning repo's workplan **file** — task-level ids do not exist in the hub, and the file is the ADR-001 source of truth anyway — and uses the file's last commit date as the honest answer to "has this moved", independent of whether the register looked. Archived workplans are searched too, so a finished-and-filed fix does not read as missing.
|
||||
|
||||
**The first run found two things the register should have known.** `RISK-F-0005`: `AUDIT-WP-0008-T04` had read `done` since 2026-08-18 — the fix landed and this repo spent three days not knowing. Now `mitigated`, embargo lifted, `public`. `RISK-F-0002`: both tracked records were closed *before the finding was filed* — `WARDEN-WP-0007` archived 2026-07-08, `FLEX-WP-0007` finished 2026-06-29, against a finding of 2026-08-18 naming `FLEX-WP-0007` as its blocker. Routed as a question to both owners rather than a conclusion.
|
||||
|
||||
Four findings carry no fix tracking at all, which the report now says out loud instead of leaving an empty field.
|
||||
|
||||
### T02 — An intake path for incidents and external reports
|
||||
|
||||
```task
|
||||
id: RISK-WP-0005-T02
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
|
|
@ -97,11 +103,15 @@ Three pieces, and only the first is this repo's alone:
|
|||
**Non-goal:** a bug bounty, a disclosure programme, or any commitment to
|
||||
outside parties. The ask is a route, not a promise.
|
||||
|
||||
Completed 2026-08-21. `docs/method/intake.md`. Incident intake: file first and grade within the hour, `first_observed` because obligations run from it rather than from the grade, `instant` cadence until it is over, and **escalation immediately rather than batched** — the batching rule protects the operator's attention and a 72-hour notification clock outranks that. `RISK-POL-0005` is wired to `first_observed`, which is what starts the clock. Severity gained a live-incident section: `L4` is what that band was always for.
|
||||
|
||||
External report: **no address exists anywhere in the estate** and creating one is not this repo's call. Routed to `the-custodian` and `policy-nexus` with a concrete proposal — RFC 9116 `security.txt` on the published surface, reports routed here, `reported_by: external` — and an explicit non-goal: a route in, not a bounty, a timeline or safe harbour.
|
||||
|
||||
### T03 — Define the production transition
|
||||
|
||||
```task
|
||||
id: RISK-WP-0005-T03
|
||||
status: todo
|
||||
status: done
|
||||
priority: high
|
||||
```
|
||||
|
||||
|
|
@ -118,11 +128,15 @@ tell the repos that carry re-score obligations.
|
|||
|
||||
Custodian decision on the definition; the plumbing is ours.
|
||||
|
||||
Completed 2026-08-21 as a proposal; the definition is the custodian's. `docs/method/production-transition.md` defines it by **what is held rather than what was announced**: the first moment any system holds, processes or decides about real external data. Which means it can happen by accident — one real signup, one migrated contact list — and cannot be reversed by deleting the data afterwards.
|
||||
|
||||
Declared by the custodian, never by this register: a risk register that unilaterally declares the estate to be in production has appointed itself. What the register does is notice and ask. Five re-scores, two acceptances ending and six dormant policies activating are listed as what fires on the day.
|
||||
|
||||
### T04 — Stop claiming a surface we do not have
|
||||
|
||||
```task
|
||||
id: RISK-WP-0005-T04
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
|
|
@ -137,11 +151,13 @@ the wording rather than editing it.
|
|||
Small, and exactly the class of claim this register grades other repos down
|
||||
for: a stated control that is not there.
|
||||
|
||||
Completed 2026-08-21. `README.md` now says the repo serves nothing yet and that publication runs through `policy-nexus`, with three documents waiting for an address. `INTENT.md` is the constitutional document and its amendment is the custodian's — the wording is proposed to them rather than edited here.
|
||||
|
||||
### T05 — Give escalation a delivery guarantee
|
||||
|
||||
```task
|
||||
id: RISK-WP-0005-T05
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
|
|
@ -156,11 +172,13 @@ a re-raise once, per the adopted rule's "raised again once" clause. Not a
|
|||
weekly nag; the rule is explicit that repetition until someone answers is how
|
||||
the operator becomes the queue.
|
||||
|
||||
Completed 2026-08-21. Escalation carries a delivery state — `sent → seen → answered` — with `escalation_sent` beside it, and `make check` reports how long each has gone unacknowledged. At seven days it says so and the escalation is raised **once** more, per the adopted rule, after which the default applies and is recorded. This is the fix the register applied to its own inbox on 2026-08-19 and had not applied to the path that matters more.
|
||||
|
||||
### T06 — Make a lie about stability impossible to miss
|
||||
|
||||
```task
|
||||
id: RISK-WP-0005-T06
|
||||
status: todo
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
|
|
@ -177,11 +195,13 @@ Two cheap defences:
|
|||
register for longer than the shortest rung by some margin, `make check` says
|
||||
so first, before anything else.
|
||||
|
||||
Completed 2026-08-21. Two defences against the rung lying: `checked_by` recorded on every check (`RISK_CHECKED_BY`, so an agent names itself rather than inheriting a unix login), and a **heartbeat** that prints before anything else in `make check` when nothing anywhere in the register has been checked for two days. A `1q` rung means "stable for a quarter" and "nobody looked for a quarter", and the heartbeat is what separates them.
|
||||
|
||||
### T07 — A coverage model
|
||||
|
||||
```task
|
||||
id: RISK-WP-0005-T07
|
||||
status: todo
|
||||
status: done
|
||||
priority: low
|
||||
```
|
||||
|
||||
|
|
@ -200,6 +220,10 @@ a maturity model, and not this repo assessing anyone.
|
|||
**If this task grows past a page it becomes its own workplan.** Coverage is a
|
||||
different problem from triage and should not quietly absorb this one.
|
||||
|
||||
Completed 2026-08-21, minimal and within its escape clause. `tools/coverage.py`, behind `make coverage`, counts what the register has heard from. First run: **7 of 117 registered repos have ever appeared in a finding; 110 never have.**
|
||||
|
||||
That is not 110 clean repos and the report says so — it is 110 repos the register knows nothing about, against an estate whose own evidence is that looking tends to find something. Recorded as an update to `RISK-N-0003` rather than promoted: there is still no owner for estate-wide detection and still no defect to route. What changed is that the gap has a size, which is the difference between an argument and a measurement.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- **No monitoring.** `RISK-N-0003` stands as a note. A register that grows
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue