Skip to content

Triage and entitlements

Proving an exploit works and deciding it is a problem are different questions. Everyone in a company being able to read everyone’s chat is trivially provable and is also how the product works. bugb separates those two questions and keeps them separate: the verdict measures, the triage argues.

A verdict is evidence. A triage assessment is an argument about whether the evidence should be reported and at what severity. It is written alongside the finding, and it changes nothing about it.

Every advisory says so in its own words:

This is an argument, not a measurement: the verdict and the evidence above are unchanged, nothing here is suppressed, and whether to report remains the operator’s call.

That sentence is the design. Triage never edits a verdict, never removes a finding, and never stops a probe from running.

A real record from report/triage.json:

{
"key": "app.allocations::idor@app/routes/index.js#62ad24636cfe",
"disposition": "informational",
"rubric_failed": ["no-boundary-crossed"],
"min_privilege": {
"profile": "",
"role": "",
"measured": false,
"exercised": ["np-attacker (user)", "np-victim (user)"],
"missing_experiment": "the probe evidence names 2 identities (np-attacker (user), np-victim (user)) and records no successful request for any of them, so it does not say which one proved the finding — re-run it recording the engine's `_cxg_actor` profile on the request that succeeded",
"source": "evidence-fallback"
},
"severity_by_config": {"default": "critical", "hardened": "unstated"},
"rebuttal": "#app.allocations names no @boundary in the project's threat model, so nothing here crosses a trust boundary the vendor asserts.",
"counter_argument": "The absence of a @boundary declaration is an annotation gap, not a vendor statement. If this asset does sit behind a trust boundary, annotating it reverses this closure without changing any fact about the finding.",
"decided_by": "rubric",
"unresolved": ["entitled-principal", "out-of-documented-scope", "already-known"]
}

Four things in there are worth naming.

rebuttal and counter_argument. The assessment writes the maintainer’s strongest reply and the answer to it. You get the argument you are walking into, not a score.

unresolved. Three rubric items nothing could answer. They are listed as open questions rather than resolved in either direction — unanswered, they demote nothing, but they are what to go and settle before sending a report.

severity_by_config. Two figures on purpose: default critical, hardened unstated. A single contested severity is what an argument gets lost over — score the default deployment and the maintainer answers from the hardened one, both correctly.

min_privilege.measured: false. The assessment says it does not know which identity proved the finding, and states the experiment that would settle it. It does not guess.

An entitlement is the durable form of “this is by design”: an @entitles annotation declaring that a role is meant to have a capability, citing where that intent is written down. It lives in the code, so it is reviewed in a pull request and survives the next run.

In the VS Code extension it is two commands — Propose an entitlement… and Review entitlement proposals, whose title states its own consequence: writes @entitles to your source.

An entitlement argues about reporting only. The probe still fires, the evidence is still collected, and the threat still appears in the model. It changes what the finding is called, not whether it was tested.

Some findings cannot be closed on entitlement grounds — one user reaching another user’s data, tenant isolation, ownership. “The admin is allowed to” is not an answer to “user A read user B’s records”.

Two further limits, both visible in the record above:

  • Triage is per finding, not per campaign. It says nothing about whether the run’s coverage was adequate.
  • decided_by: "rubric" means a fixed rubric produced this disposition. A rubric is a policy, and a policy encodes someone’s opinion about what is worth reporting — treat the disposition as an opening position, not a ruling.