Skip to content

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

python3.12 scripts/demo_server.py
Demo API: http://127.0.0.1:8765

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 run --config fixtures/veridor.demo.json
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, HEAD or OPTIONS request 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:

python3.12 scripts/demo_server.py --vulnerable

Now run the same command. Nothing about the configuration changes.

veridor run --config fixtures/veridor.demo.json --sarif veridor.sarif
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_marker on 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 an excludes_marker that has no control behind it.
  • excludes_marker on 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:

ls .veridor/runs
20260805T024650Z-003529.json
20260805T024708Z-cc1dc3.json

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).

veridor serve

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, run and mutate each ask, and in what order.
  • run in full — baselines, sequences, session state, write safety, boundary coverage.
  • Configuration reference — every key in veridor.json, for pointing this at your own API.