← back to writing

A risk register is a claim about the future

A register that can't reproduce its own severity scores is a list of opinions in a spreadsheet. How do you score, close, and phase risk so it survives a skeptical engineering team?

The temptation with any register is to let severity mean whatever felt right when you wrote the row. I don’t think that survives contact with a skeptical engineering team, and it shouldn’t, because a register that can’t reproduce its own scores isn’t really a register, it’s a list of opinions wearing a spreadsheet.

Likelihood and impact, not vibes

Likelihood reflects attacker prerequisites and exposure: does the path require an authenticated account, a foreign identifier, privileged access, or nothing at all. Impact reflects tenant scope, reversibility, recoverability, and business consequence, not “how bad does this sound” when you read the finding out loud. This follows a common risk-assessment model: likelihood and impact evaluated against explicit organizational criteria, with every assumption recorded so the result can be reproduced, applied at finding granularity rather than control-family granularity. Both dimensions are scored one to five, and the tiers are fixed up front so they reproduce: on the resulting five-by-five scale, 1 to 9 is low, 10 to 15 is medium, and 16 to 25 is high, with one deliberate override, a risk whose impact is catastrophic on its own, permanent data loss or an unreconstructable production system, is treated as high regardless of likelihood. And any assumption that changed a score gets written down right next to the score, because an unstated assumption is where a register quietly stops being falsifiable.

Worked example, because a scoring principle you can’t reproduce isn’t really a principle: R-07, a tenant-scoped write path lacking target authorization, scored likelihood 3 of 5. It requires an authenticated account, a real but not high barrier, and a foreign-tenant identifier, which turned out to be obtainable through a legitimately accessible listing endpoint elsewhere in the product, not through guessing. It scored impact 5 of 5, because the affected helper was shared code reused across every tenant context, so the consequence class was full-platform even though any single exploit attempt only touches one foreign record at a time. Score of 15, a real demonstrated path with a moderate barrier and a severe consequence class, not a worst-case assumption stacked on a worst-case assumption. That’s the difference between defensible severity and severity theater.

The register, read as a business document

Fifteen rows, and every one maps to an owner and a time horizon, not just a technical description. R-01 and R-02, high, no independent recovery path and production not traceable to reviewed source, both zero-to-seven days, platform owner and operations respectively, because a destructive event or an unreconstructable deployment are both permanent-loss scenarios with no partial credit. R-03, conditionally high, untrusted or conditional edge transport, severity here literally depends on whether the affected hosts are public or intentionally private under managed trust, which is exactly the kind of nuance a flat severity label erases if you don’t write the condition down next to it.

The medium tier is where most of the actual engineering work lives: object-storage and database runtime identities carrying administrative authority (R-04, R-05), alternate listeners bypassing intended edge controls (R-06), the tenant-scoped write path itself (R-07), a renderer input crossing a browser execution boundary (R-08), incomplete logging and alerting (R-09), a shared internal credential spanning all tenants (R-10), an unauthenticated telemetry endpoint (R-11), unvalidated background job payloads (R-12), and unconfirmed third-party account access and MFA status (R-13). Two low findings round it out, an out-of-order business-logic transition (R-14) and missing baseline security headers (R-15), real gaps, correctly not treated as anyone’s emergency.

R-13 is the row people underrate the most

A single compromised registrar, DNS, or email-provider account can undermine every other control in the register, including password-reset integrity, regardless of how well the application itself is built. It’s also completely outside what a repository review or a live pentest can observe directly, you can’t source-review a third party’s MFA policy. The honest move was converting it into a client evidence request rather than assuming it away or, worse, inflating it into a finding backed by no actual evidence. A register with fourteen well-evidenced technical rows and one hand-waved “probably fine” row about the domain registrar is a register that’s lying about its own confidence level.

Closure evidence, because a fix nobody verified isn’t a fix yet

Every row in the register has a matching row in the closure table, and that pairing is the actual point of the whole exercise. A finding doesn’t move to closed because a patch landed in source, we’ve already covered how little that guarantees about what’s running in production. It closes when the specific negative and positive tests defined for that risk both pass, on the deployed system, with evidence recorded.

R-01 closes on a clean-environment restore that succeeds, with recovery time recorded and retained copies unable to be deleted by production identities. R-04 and R-05 close on runtime identities that pass required operations and fail policy, identity-management, and backup-deletion tests, the negative test is the actual proof, not the positive one. R-07 closes on foreign-tenant and global writes denied before persistence while authorized same-tenant writes keep functioning. R-10 closes on credentials that are tenant or project scoped, with rotation, revocation, attribution, and unexpected-source detection all demonstrated, not just configured.

The phased roadmap, and why the phases aren’t arbitrary

Phase 0 is fact-establishment and obvious-exposure containment, record the running artifact digest, restrict alternate listeners, confirm the real public trust model, take an immediate off-box copy of relational data and object storage using credentials the application doesn’t have, assign named owners. None of this is remediation yet, it’s making sure the ground you’re about to stand on is solid.

Phase 1, zero to seven days, is the release blockers: independent recovery, deployment identity, valid edge trust, the tenant-authorization fix, the renderer-input fix. Each one blocks release for a specific, nameable reason, a destructive event is otherwise permanent, security findings can’t be tied to production, users can’t establish normal trusted transport, there’s a confirmed cross-tenant integrity path, there’s a confirmed persistent browser-execution path. Verification for each is a live positive and negative test, never a code-review sign-off.

Phase 2, thirty days, is containment and operational readiness, separating database and object-storage identities by function, running containers as non-root, moving security events off-box, adding regression tests for the exact classes of bug just fixed, documenting incident roles before an incident forces you to improvise them.

Phase 3 is strategic, project-scoped credentials replacing platform-wide tokens, row-level security as defense in depth on the highest-value tables, periodic restore drills and destructive-incident tabletop exercises, and turning the whole engagement into reusable secure-engineering standards instead of a report that gets read once and shelved.

Risk acceptance is a decision, not a default

An unresolved finding was never treated as “accepted” just because the team intended to get to it eventually. A real acceptance needs a named owner, a business justification, compensating controls, a stated residual likelihood and impact, an approval date, an expiry date, and review triggers. Anything short of that is an unpatched finding wearing a more comfortable label, and I think that label-swapping is one of the most common ways registers quietly stop meaning anything.

TL;DR

A risk register is only as good as its ability to defend its own numbers out loud. Score likelihood and impact from stated attacker prerequisites and business consequence, not from how alarming the finding sounds. Pair every row with objective closure evidence tied to a live test, not a source-code claim. Phase the roadmap by what’s actually blocking release versus what’s strategic hardening. And never let “we’ll get to it” pass as risk acceptance without an owner, a justification, and an expiry date attached.