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.