Status: early alpha.

Your authorization boundary, as a regression test.

Broken access control, where a user can reach data or actions they should not, ranks first in the OWASP Top 10 and has the most recorded occurrences in OWASP's data. OWASP notes that automated scanning tools can spot a missing access check but cannot confirm that an existing one works, and it advises teams to write their own access control tests and not deploy changes that fail them. Veridor runs those tests in a team's build pipeline, so a change that breaks an access rule the team has declared and tested can fail the build before it ships.

Sources: OWASP Top 10:2025, A01 Broken Access Control; OWASP Top 10:2017, A5 Broken Access Control; OWASP API Security Top 10:2023, API1.

Demo API · before the fix
operation invoice_owner other_tenant admin­istrator anony­mous
GET /invoices 404 404 404 404
GET /invoices/{id} 200 with invoice_owner's marker 200 with invoice_owner's marker 404 401
DELETE /invoices/{id} · · · ·
GET /admin/audit 403 403 200 401
POST /admin/audit/export · · · ·
$ veridor run --config fixtures/veridor.demo.json 1 vulnerable (confirmed): other_tenant got HTTP 200 (declared 404) carrying invoice_owner's marker EXIT=1 · the build fails
Measured on 2026-11-09 with veridor map and veridor run against Veridor's demo API (scripts/demo_server.py), with and without --vulnerable. Paths are under /api. The fix changes one cell.

What it does today

Declare what each identity is allowed to do. Veridor issues the same request as every identity, compares what happened against what you declared, and fails the build when the answer changes.

Three commands, graded verdicts, standard reports.

MAP

Sends the same read-only requests as each identity you give it and records who reached what. Its suggestions are leads to check, never findings.

RUN

Sends each request you declared as every identity, compares the answers with your access rules, and by default fails the build on any new or confirmed break.

MUTATE

Deliberately weakens your declared rules to show whether each of your checks can fail at all.

VERDICTS

Graded by evidence: confirmed means another identity's planted marker came back in the response; suspected and ungraded say less, and say so.

REPORTS

SARIF, JUnit, GitLab SAST, Code Quality and Markdown.

CI

The CI workflow passed every job on commit 38025ed (2026-11-08).

Published fixes

Built on published fixes.

Four access-control flaws found and reported by the author, each fixed by its project and published with a CVE record. Each one is now a test of Veridor.

Each test runs the project's own published release from just before the fix and the one that fixed it, and passes only when Veridor reports the flaw on the first and nothing on the second. For InvoiceShelf and hawkBit, the proof is another account's data appearing in the response; for Ghost and Domoticz, it is the server allowing an action it should refuse. All four passed on 2026-11-09.

Roadmap

Next: a first release.

Status: early alpha.

  1. Next

    Open source

    Source published under Apache-2.0.

  2. Then

    First release

    A tagged, signed release.

  3. After

    Packages

    PyPI, container image and Helm chart.

  4. Later

    Docs and listings

    Docs site, GitHub Marketplace and GitLab CI/CD Catalog.

About

An independent project.

Built by Santosh Kumar Puppala, an independent security researcher. It draws on his published research on authorization flaws.

Technical detail · proof, not inference

A status code cannot tell you what came back.

Plant a unique marker in one identity's data. If another identity's response carries it, the bytes crossed.

  1. You Plant a marker in invoice_owner's record
    specimen owner_invoice
    field: marker
  2. Veridor Send the same request as other_tenant
    GET /api/invoices/{id}
    credential: other_tenant
  3. Target Answers with the record
    HTTP 200 (declared: 404)
    body contains marker
  4. Verdict The owner's marker crossed vulnerable (confirmed)
    build exits 1
confirmed
another identity's marker was in the response
suspected
status differed; no owner-specific marker seen
ungraded
status differed; no marker configured

The product

Three verbs, asked in this order.

Discover, declare, gate, then verify the gate.

map What would a compromise reach? exit 0 whatever it finds; 3 if it measured nothing
you declare Who may do what, in veridor.json no exit code
run Does the boundary hold, and did it change? exit 1 when the --fail-on policy fires (default: a new finding, or any confirmed one)
mutate Would my checks catch a regression? exit 1 if a mutant survives: a weak declaration, not a broken target
map --baseline (drift) Diff against a matrix you recorded. exit 1 only with --fail-on-drift

What would a compromise reach?

Credentials and a spec, nothing declared. Reach is a lower bound, counted in operations.

veridor map · vulnerable demo · abridged
Observed reach (lower bound), identity "other_tenant":
  reached 1   refused 2   unresolved 0   unmeasured 2   of 5
  no share is rendered: 2 operation(s) were not measured, …

[marker_reach] candidate -- other_tenant on GET /api/invoices/{id} [specimen owner_invoice]

EXIT=0  # nothing map finds fails a build

Does the boundary hold, and did it change?

The gate: graded findings, diffed against a baseline.

veridor run · vulnerable demo · abridged
5/6 probes matched their declaration, 1 vulnerable, 0 unexpected denial, 0 inconclusive
Evidence: 1 confirmed, 0 suspected, 0 ungraded, …

- [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 …
EXIT=1   # fixed demo: 6/6 probes matched, EXIT=0

Would your checks catch a regression?

Six operators rewrite your declarations. A survivor is a check that cannot fail.

veridor mutate · mutation fixture · abridged
Mutation score: 0.92  (12 killed / 13 generated)
  survived 1  aberrant 0  errored 0  not applicable 2  skipped 3
  0.86 of 14 generated + limited (1 could not be evaluated)

[SURVIVED] M1 invert-denial -- check 'widget-read-permissive', identity outsider
EXIT=1  # one mutant survived

A regression check with nothing declared.

Not a separate command. It sees only what changed, in cells both runs measured.

veridor map --baseline … --fail-on-drift opened · abridged
Access-matrix drift — 2 change(s), 2 that mean more access
  drift saw 60% of the grid: 12 of 20 comparable cell(s)

  [reach_opened] GET /api/invoices/{id} other_tenant [specimen owner_invoice]
      refused -> reached
  [marker_appeared] GET /api/invoices/{id} other_tenant [specimen owner_invoice]
      absent -> present
EXIT=1

Measured, with bounds

Two numbers, two different questions.

scripts/noise_floor_study.py, seeds 1 to 9, ten runs each. Drawn to scale.

0 of 90 runs

fired a false drift gate on a server whose boundary never changes

95% CI [0.0%, 4.1%] (Wilson)

One synthetic server, one noise profile. Not proof of zero.

87 of 90 runs

caught one planted boundary opening

96.7% · 95% CI [90.7%, 98.9%]

One fixture. 0% where the baseline did not measure the cell.

Neither is a detection rate for authorization bugs in general. Separately, CI requires the verdict to flip across a set of published fixes, including the four listed above.

Built for the pipeline

Bounded, and it says unknown.

STDLIB ONLY

0 third-party runtime imports: an AST scan of all 25 source files, which catches a planted import.

UNANSWERED IS NOT A PASS

A probe that drew a 429, a 5xx or a timeout is inconclusive. With --require-complete, which every integration turns on, it exits 3; without it, only a run where no probe was answered exits 3.

MAP: READ-ONLY, RATE LIMITED

GET and HEAD only, 2 requests a second by default, at most 10. run sends what you declared, unthrottled; --read-only refuses writes.

REPORTS

SARIF, JUnit, GitLab SAST, Code Quality, Markdown. No SARIF upload to code scanning observed yet.

CI/CD integrations

Where the gate has been run end to end.

By default every integration fails the job on any non-zero exit.

0 pass
the selected gate passed
1 fail
the gate failed: a finding the --fail-on policy selects
2 refused
run: a configuration, coverage or safety refusal (mutate also uses 2 when nothing was measured or a mutant was unresolved)
3 incomplete
gate passed, but something it needs went unmeasured, such as no probe answered (with --require-complete, any probe unanswered)

Also 70 (internal error, never a finding), 130 and 143 (interrupted; teardown runs).

  • GitHub Action

    Ran in a local harness (a vulnerable target fails the step, a fixed one passes) and on GitHub-hosted Linux and macOS runners in the project's CI.

    local harness · GitHub runners
  • GitLab CI/CD component

    The job's own rendered script, run in Docker against the demo API: 158 checks covering exits 0, 1, 2, 3 and 143 and every report (2026-09-27).

    Docker
  • Argo CD PostSync gate (Helm)

    A kind cluster with Argo CD v3.5.3: a vulnerable target fails the sync, a fixed one succeeds.

    kind cluster
  • Kustomize plain Job

    The same kind run: the vulnerable Job fails, the fixed one completes.

    kind cluster
  • Argo Rollouts analysis

    The same kind run with Argo Rollouts v1.10.0: a vulnerable canary is aborted, a fixed one promoted.

    kind cluster
  • Container image

    Built in CI and run as the gate, and as the job image in the GitLab run above.

    CI
  • Zipapp

    Built, then run on Python 3.11 against the demo API: vulnerable exits 1, fixed exits 0.

    Python 3.11

Also provided: configuration for Jenkins, Azure Pipelines and AWS CodeBuild. For each, the gate's shell runs on a host against the demo API and the file is checked by that platform's linter, schema or reference. The Kubernetes entries ran on kind clusters; the GitLab, Jenkins, Azure and CodeBuild entries ran outside their own platform, as described.

What it does not claim

The limits are the product.

It consumes the policy you wrote; it cannot audit it.

  • A finding means behaviour differed from your declaration, which may itself be wrong.
  • Confirmed proves the marker crossed, not that your policy is right.
  • Coverage says a cross-identity denial is asserted, never that it is enforced.
  • A green build means nothing the gate fails on was found on this run.
  • Not a scanner. No route discovery (map probes only the spec you give it), no policy invention, no fuzzing or payloads.

Install

From source, at release.

Python 3.11 or newer, standard library only. Install steps will be published with the source.