Skip to content

Verdicts and evidence grades

Two independent axes describe every probe veridor run issues. The verdict says how the observation compared with your declaration. The evidence grade says how strong the evidence behind it is. A finding carries both, and neither implies the other.

Every one of these is a comparison against your declaration. Read them alongside what a finding does and does not mean.

Verdicts

Verdict Means Console label
safe The response matched the declared expectation. Matched
vulnerable A reachable target served a response the declaration did not allow: a status outside the declared set, or a response carrying another identity's marker. Vulnerable
unexpected_denial The target did not serve the request: a status outside the declared set and outside 2xx (401, 403, 404, and also 400, 405, 409, a 302 and the like), other than the statuses graded inconclusive, and no marker was observed. Unexpected denial
inconclusive Execution or the marker oracle could not produce a verdict. Inconclusive

safe is reported on the console as matched, deliberately. It is agreement with your configuration for one request, not a finding of security. An expectation that accepts both access and denial matches trivially.

unexpected_denial is separated from vulnerable because direction matters. vulnerable claims the target handed out something it should not have; a refusal handed out nothing. A missing route, an unshipped feature, or a 404 where 403 was declared are all failed declarations and all still findings — they still trip the gates — but calling them vulnerable would contradict Veridor's own model, where 403 and 404 are denials.

inconclusive is what a probe gets when nothing was measured. A 429, a 5xx, a proxy's 407 (Proxy Authentication Required), a transport error, a timeout, a body too large to scan, a session that never established, a response an intermediary announced as cached — none of these is an answer from the authorization layer. It establishes nothing in either direction.

inconclusive does not trip --fail-on

new, any, confirmed and new+confirmed fire on vulnerable and unexpected_denial only. A gate that fires when nothing was measured is not strict — it is wrong, and it is the reason gates get switched off.

Coverage is enforced through a different door: --require-complete exits 3, and a run where not one probe was answered exits 3 with no flag at all. See exit codes.

Evidence grades

Grade Means
confirmed The response carried another identity's declared marker.
suspected The observed status differed from the declared one, and no owner-specific marker was observed: either the marker was not observed, or a credential Veridor fabricated got it back too. Exposure is unproven.
ungraded The observed status differed from the declared one and no marker was configured for this check.
n/a Inconclusive: the request failed, or the oracle could not be verified.

What confirmed actually establishes

The full claim, stated at the strength it has:

The owner's declared marker was observed in another identity's response. That is proof the string crossed, and proof of data flow insofar as the marker is unique to that identity's data — a premise you supply by planting it. It is never proof of policy.

Each clause is load-bearing.

Proof the string crossed is what Veridor measured directly. Those bytes were on the wire, in that response, to that identity.

Proof of data flow insofar as the marker is unique. The uniqueness premise is the operator's, not Veridor's. The earlier wording was falsified — "bytes that exist only in the owner's data" — against a target returning the same build id to every caller, where that id happened to be the declared marker. Two byte-identical responses, nothing crossed any boundary, and confirmed was issued.

So the grade caps at suspected when the premise is disproved. For every GET, HEAD or OPTIONS request carrying an excludes_marker, whatever statuses it accepts, run sends one probe carrying a credential it fabricated — veridor-sham-…, which authenticates to nothing. If that fabricated caller gets the same marker back, the string is not owner-specific, the data-flow sentence is not emitted at all, and the grade caps at suspected:

- [vulnerable (suspected)] invoice-tenant-isolation / other_tenant: Identity
  'other_tenant' received HTTP 200 (declared: one of [404]) and the response
  carried 'invoice_owner''s marker -- but so did a request carrying a credential
  Veridor fabricated, which the target cannot have recognised. 'invoice_owner''s
  marker is therefore not unique to its data, so the string crossing proves
  nothing about data flow. Plant a marker that appears only in that identity's
  records.

Absence of that evidence is not proof of uniqueness. A confirmed grade means no disproof was found, not that uniqueness was established. Where no fabricated credential is sent (a write, no credential header to shape it on, --no-prove-liveness) or its response could not be scanned, the console and liveness.marker_uniqueness_not_checked say the marker's uniqueness was not checked there, and a crossing is graded on your premise alone.

Never proof of policy. Whether the receiving identity was entitled to that data is a statement you made in the configuration. An over-strict declaration produces confirmed on entirely legitimate behaviour; DeclarationScopeTest in tests/test_marker.py builds that case. See the oracle proves data flow, never policy.

Where the grades come from

An exclusion is only meaningful if presence was demonstrable, so Veridor requires a control behind every excludes_marker and refuses to load a configuration without one. The two controls are not equivalent:

Control Proves Grade effect
Same-check control — the owner asserts contains_marker at the very path under test The marker is observable at this path, so absence in the attacker's response is falsifiable The strong control. Prefer it whenever the API's shape allows it
Identity marker_probe — the owner asserts its marker at some other endpoint The marker is live somewhere, not that the path under test can surface it The run says so on the console, in oracle_disclosures, and in runs[0].properties.oracleDisclosures in SARIF

If the owner's own data comes back without the owner's marker, this path cannot surface markers at all, the exclusion is unfalsifiable here, and the finding is inconclusive — an identity marker_probe of some other endpoint cannot override that.

Which rule each verdict is reported under

Rule id Verdict Grade
VERIDOR-AUTHZ-001 vulnerable or unexpected_denial confirmed, suspected or ungraded
VERIDOR-LEAK-001 vulnerable confirmed, or suspected when the target served a credential Veridor fabricated and returned the marker to it
VERIDOR-EFFECT-001 vulnerable confirmed, or suspected when no earlier step observed the owner's marker
VERIDOR-EXEC-001 inconclusive n/a
VERIDOR-ORACLE-001 inconclusive n/a
VERIDOR-CACHE-001 inconclusive n/a
VERIDOR-SEQ-001 inconclusive n/a
VERIDOR-SESSION-001 inconclusive n/a

VERIDOR-LEAK-001 is the case where the status matched your declaration and the marker crossed anyway. A status-only oracle cannot see it.

VERIDOR-VACUOUS-001 appears in SARIF but is not a finding and carries neither a verdict nor a grade: it is a statement about a declaration that accepts both access and denial. See SARIF.

Both are properties, not identity

Verdicts and evidence grades are finding properties. Neither is part of the finding fingerprint.

That means adding markers to an existing suite never invalidates your baseline, and this vocabulary can be refined without reporting your whole suite as new. The fingerprint is a digest over the check id, the identity, the finding kind, the method, the path, the request-body digest and the expected statuses — and nothing else.

The consequence worth knowing: the fingerprint alone cannot tell you a finding got worse. The regression diff reports that separately.

Diff status Means
new Present now, absent from the baseline.
resolved Present in the baseline, absent now.
unchanged Present in both.
escalated Unchanged, but the grade rose to confirmed since the baseline.

The run document's own status

A run is passed, failed, or inconclusive — the same three words, one level up. It is not the exit code: status is computed from the findings alone, while the exit code depends on --fail-on and the other flags. The decision the process exited with is recorded beside it, in the run document's gate object. See exit codes.

map uses a different vocabulary, on purpose

The access matrix is not a set of findings and does not borrow this vocabulary. A cell is one of four things:

Reach Means
reached This identity was served this operation.
refused This identity was refused.
indeterminate The response settled nothing either way.
unknown Nobody measured this cell — not probed, skipped as mutating, over budget, no specimen, ignored, rate limited, a server error, a transport error, a proxy's 407, or aborted.

unknown is never rendered as "no access", and a cell nobody could measure is never reported as a cell whose access changed. Mixing the two vocabularies is how a hypothesis gets read as a finding, which is why the local viewer keeps one tab per verb.

Related: what a finding does and does not mean, SARIF and code scanning, run.