Your first finding¶
You want to see what a Veridor finding looks like before you point it at anything of your own. You do not need an API for that. The repository ships a deterministic demo target with a real cross-tenant bug behind a flag, and a configuration that declares the boundary the bug breaks.
Sixty seconds: run the gate green, flip the target, run the same command, read the finding.
You need a checkout
The demo server and the fixtures live in the git repository, not in the
distribution — a wheel carries the veridor package and nothing else.
Running the commands below against an installed wheel gives you
veridor: configuration not found: fixtures/veridor.demo.json, which reads
like a broken install and is not one. See
install from a checkout. The source
repository is private until it is published, so for now this page shows
what the demo prints rather than something you can follow.
1. Start the demo target¶
It serves invoices for two tenants and an admin-only audit log, in memory, with fixed tokens. (It carries a few more routes that the other guides use.) Leave it running in its own terminal.
If port 8765 is already taken
The demo refuses to start rather than share the port, and says why:
demo_server: cannot listen on 127.0.0.1:8765: [Errno 48] Address already in use
Another process already holds 127.0.0.1:8765. This demo did NOT start.
Anything you point at that address next will be probing that other process, and a green result would say nothing about this demo.
Free the port, or run somewhere else and point the tool at it:
python3 scripts/demo_server.py --vulnerable --port 8799 &
veridor run --config fixtures/veridor.demo.json --base-url http://127.0.0.1:8799
Whatever else owns that port would answer the probes instead, and a green
run against it would say nothing about this demo. Every run prints its own
Target: line, so you can always see what the verdict above it was a
verdict about.
2. Run the gate¶
veridor: liveness controls -- 2 extra request(s) carrying a credential Veridor fabricated, one per GET, HEAD or OPTIONS request whose declaration accepts only denial statuses. Without them an expired token reads as a boundary that held. A denial the fabricated credential's answer does not tell apart counts as unproven when the identity presents a credential, some identity was served in the same check, and nothing else in this run proves the identity live (a 2xx it drew on a request where an identity with no credential, or the fabricated credential, was refused; or its marker_probe). The value authenticates to nothing; pass --no-prove-liveness to skip them.
Veridor run 20260805T024650Z-003529: 6/6 probes matched their declaration, 0 vulnerable, 0 unexpected denial, 0 inconclusive
Target: http://127.0.0.1:8765
Regression diff: 0 new, 0 resolved, 0 unchanged
Evidence: 0 confirmed, 0 suspected, 0 ungraded, 0 escalated since the baseline
Scope: Veridor evaluates the expectations you declared. A finding means observed behaviour differed from your declaration; it does not establish that the declaration is correct.
Pass: A matched probe means the target agreed with your declaration for that one request. It is not evidence that the boundary is enforced, and an expectation that accepts both access and denial matches trivially.
Exit code 0. Six probes, six agreements with the file you passed in.
Three things in that output are worth reading rather than skipping.
- The liveness line. For each
GET,HEADorOPTIONSrequest whose declaration accepts only denial statuses, Veridor sends one extra probe carrying a credential it fabricated. Without it, an expired token reads as a boundary that refused this caller. The fabricated value authenticates to nothing, and the controls are on by default. Scope:— the standing caveat. A finding means observed behaviour differed from your declaration. It does not establish that the declaration is correct. It prints on green runs too.Pass:— a matched probe is agreement with your configuration for one request. It is not evidence that the boundary is enforced.
3. Break the target¶
Stop the demo server and start it again with the bug switched on:
Now run the same command. Nothing about the configuration changes.
veridor: liveness controls -- 2 extra request(s) carrying a credential Veridor fabricated, one per GET, HEAD or OPTIONS request whose declaration accepts only denial statuses. Without them an expired token reads as a boundary that held. A denial the fabricated credential's answer does not tell apart counts as unproven when the identity presents a credential, some identity was served in the same check, and nothing else in this run proves the identity live (a 2xx it drew on a request where an identity with no credential, or the fabricated credential, was refused; or its marker_probe). The value authenticates to nothing; pass --no-prove-liveness to skip them.
Veridor run 20260805T024708Z-cc1dc3: 5/6 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive
Target: http://127.0.0.1:8765
Regression diff: 1 new, 0 resolved, 0 unchanged
Evidence: 1 confirmed, 0 suspected, 0 ungraded, 0 escalated since the baseline
Scope: Veridor evaluates the expectations you declared. A finding means observed behaviour differed from your declaration; it does not establish that the declaration is correct.
Pass: A matched probe means the target agreed with your declaration for that one request. It is not evidence that the boundary is enforced, and an expectation that accepts both access and denial matches trivially.
- [vulnerable (confirmed)] invoice-tenant-isolation / other_tenant: Identity 'other_tenant' received HTTP 200 (declared: one of [404]) and the response carried 'invoice_owner''s marker: 'invoice_owner''s declared marker was observed in 'other_tenant''s response. That is proof the string crossed, and proof of data flow insofar as the marker is unique to 'invoice_owner''s data. You declared that 'other_tenant' must not receive 'invoice_owner''s data, and Veridor measured that it did. Whether that declaration is correct is not something Veridor can check.
Confirm the declared expectation is the policy you intend, then enforce that identity, role, and tenant authorization on the target. If the declaration is wrong, correct the configuration instead.
Exit code 1. That is the gate failing a build.
veridor.sarif carries the same finding for code scanning: rule
VERIDOR-AUTHZ-001, level error, a stable
partialFingerprints.veridorFindingId/v1, and the verdict, evidenceGrade
and markerState properties as SARIF properties rather than prose. See
SARIF and code scanning.
What made it confirmed¶
The check that failed is one object in fixtures/veridor.demo.json, below with
its name dropped and its status arrays put on one line:
{
"id": "invoice-tenant-isolation",
"request": {"method": "GET", "path": "/api/invoices/inv-100"},
"expect": {
"invoice_owner": {"status": [200], "contains_marker": "invoice_owner"},
"other_tenant": {"status": [404], "excludes_marker": "invoice_owner"},
"anonymous": {"status": [401]}
}
}
invoice_owner declares a marker: a high-entropy string
(vdr-Xk7Qm2Ls9Rt4Vb8Nc3Zp6Wf1) that the demo plants inside invoice inv-100,
which belongs to that tenant and no other. The check asserts two things about
it, and they work as a pair:
contains_markeron the owner is the control. It proves this path can surface the marker at all. Without it, absence in somebody else's response would be unfalsifiable, and Veridor refuses to load anexcludes_markerthat has no control behind it.excludes_markeron the outsider is the assertion. The outsider's response must not carry the owner's string.
A status code alone cannot tell you whether any data moved. 200 where you
declared 404 is a disagreement with your declaration; on its own it grades
suspected or ungraded. What made this one confirmed is that the outsider's
response literally contained a value that exists only in the owner's data.
| Grade | What was observed |
|---|---|
confirmed |
The response carried another identity's declared marker |
suspected |
The status differed from the declaration; the marker was not observed |
ungraded |
The status differed from the declaration; no marker was configured for this check |
n/a |
Inconclusive: the request failed, or the oracle could not be verified |
confirmed is the strongest evidence Veridor produces, and its bound is stated
in the finding itself: it is proof that 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. A
declaration that forbids access somebody is genuinely entitled to produces the
same confirmed finding, because the data flow was real and only the
declaration was wrong.
So triage a confirmed finding by checking your declaration first. It is the
cheaper of the two hypotheses to eliminate.
Verdicts and evidence grades has the full vocabulary;
claims and limits has what each measurement never claims.
Where the run went¶
Runs are persisted under .veridor/, and .veridor/latest.json points at the
newest:
Every run rewrites latest.json, so it cannot be the --baseline of a run
into the same --output-dir: that is refused with exit 2, because the re-run
would compare a live finding against itself. Copy it to a file of its own and
pass that.
Response bodies are never persisted. A run document holds status, duration,
body size, a SHA-256 digest, and — when a marker is configured — booleans
derived from an in-memory scan. No response text and no headers are written to
disk, and the marker literal itself never reaches an artifact. A value expanded
from a ${NAME} is written as ${NAME}, except one under 8 characters, which
the command names on stderr (safety posture).
opens a local, read-only viewer on http://127.0.0.1:8787 over that directory, with one tab per verb. It never writes and never issues a probe.
What the exit codes mean¶
| Code | Meaning |
|---|---|
0 |
Nothing the gate fails on was found, and coverage (if requested) was measured. Not the same as "every expectation held": a finding the policy does not gate on exits 0 too |
1 |
The gate failed |
2 |
Configuration, credential, specification, path-safety, or undefined-coverage error — including a --read-only refusal, an unset or empty ${VAR}, and an --output-dir or report path this process cannot write, each refused before any request |
3 |
The gate passed but something else did not: a sequence teardown was unclean (--require-clean-teardown), a probe went unanswered or a denial is unproven (--require-complete), or not one probe was answered at all — that last needs no flag. A real finding always wins, so 3 never replaces 1 |
Exit 3 is the one worth knowing about before you wire this into CI: a run that
measured nothing is neither a pass nor a failure, and it says so instead of
going green. Exit codes covers the rest, including which
--fail-on policy decides between 0 and 1.
Next¶
- The three verbs — what
map,runandmutateeach ask, and in what order. runin full — baselines, sequences, session state, write safety, boundary coverage.- Configuration reference — every key in
veridor.json, for pointing this at your own API.