Skip to content

SARIF and code scanning

You want findings to land where your team already reads them, and you want the same finding to stay the same finding across runs rather than reappearing as new every Tuesday. veridor run writes SARIF 2.1.0 for exactly that.

Only run writes SARIF. map produces none — a candidate is a hypothesis, and a hypothesis in a code-scanning UI is read as a finding. mutate produces none either: a surviving mutant is a fact about your declaration, not about your target.

Write it

veridor run --config veridor.json --sarif veridor.sarif

Or as one of several reports from the same run, with --report sarif=veridor.sarif. Regenerate it from a persisted run without repeating a single probe:

veridor report --from-run current.json --format sarif --output veridor.sarif

The older spelling, veridor report --input current.json --sarif veridor.sarif, still works and prints Wrote SARIF report to veridor.sarif. See reports for the other formats.

Upload it to GitHub code scanning

- uses: Veridor-authz/veridor@<full-commit-sha> # vX.Y.Z, after the first release
  id: veridor
  with:
    config: veridor.json
    sarif: veridor.sarif
- if: ${{ always() && steps.veridor.outputs.sarif != '' }}
  uses: github/codeql-action/upload-sarif@1c5b675653bb5c22dbe9b12b556ec555138e09fd # v4.38.1
  with:
    sarif_file: ${{ steps.veridor.outputs.sarif }}
    category: veridor

Veridor-authz/veridor@<full-commit-sha> is a placeholder: no release exists yet and the repository is private (see the GitHub Action). The job needs security-events: write. The always() guard is what gets the file uploaded on the run that failed the build, which is the only run anyone wants to look at. The sarif output guard covers exit 2: the Action sets that output only when Veridor wrote the SARIF during that step, so after a refusal it is empty and nothing is uploaded. A hashFiles('veridor.sarif') guard, which the example used before, is true for a file an earlier step left at that path, and would upload that file as this run's.

Nothing on this page has been observed inside GitHub code scanning. No upload of Veridor SARIF to code scanning has been observed. What follows describes the file Veridor writes, measured locally, and GitHub's documented rules for reading it.

What identifies the run

Field Value Why
tool.driver.name Veridor
tool.driver.semanticVersion the running Veridor's version (0.1.0 measured; that version string is in the source, and nothing has been released under it) read from veridor.__version__ at write time; it used to be a literal
runs[].automationDetails.id veridor/<run_id> (measured: veridor/20260924T073701Z-90e165) GitHub's documented rule takes the category from everything before the last /, so every run files under one category, veridor, while the id stays unique. The id used to be the bare run id, which has no /

Passing category: veridor to upload-sarif, as above, names the same category explicitly.

security-severity and tags

Every rule carries properties.tags: ["security", "authorization"]. GitHub's documentation says it reads properties.security-severity (a number from 0.0 to 10.0, as a string) only from rules tagged security, and ranks alerts by it. Veridor gives a severity only to the rules whose results are findings about the target:

Rule security-severity
VERIDOR-AUTHZ-001 7.5
VERIDOR-LEAK-001 8.8
VERIDOR-EFFECT-001 8.8
every other rule none

The rules that mean nothing was measured (EXEC, ORACLE, SEQ, SESSION, CACHE) and the declaration note (VACUOUS) carry the tag and no severity: a security severity on a probe that never ran would present an unmeasured boundary as a rated weakness. None reaches 9.0, GitHub's "critical" band, because every finding is graded against a declaration Veridor cannot audit. Measured on the vulnerable demo run: the four always-declared rules carried exactly these properties. How GitHub ranks or files them has not been observed.

The Action writes SARIF in run mode whether or not you name a path — sarif defaults to veridor.sarif. See the GitHub Action.

Findings are also emitted as GitHub file annotations when GITHUB_ACTIONS is set in the environment, anchored to the same line the SARIF result uses. That is for the pull-request diff view; the SARIF upload is for the security tab.

Rule ids

Four rules are always declared in tool.driver.rules, whether or not the run produced a result for them. The rest are appended only when a result references them, so SARIF for a configuration that never hits them stays byte-identical across releases.

Rule id Level Declared Fires when
VERIDOR-AUTHZ-001 error always The observed HTTP status differed from the status declared for that identity.
VERIDOR-LEAK-001 error always The status matched the declaration, but the response carried another identity's marker.
VERIDOR-EXEC-001 warning always The request could not be completed, so nothing was measured for this identity.
VERIDOR-ORACLE-001 warning always The marker was not observed where the configuration says it should be, so marker-based conclusions carry no evidence either way.
VERIDOR-VACUOUS-001 note on reference A declared expectation accepts both access and denial, so it asserts nothing about the boundary.
VERIDOR-SEQ-001 warning on reference A step the rest of a sequence depends on did not complete, so the later steps were never executed.
VERIDOR-EFFECT-001 error on reference An identity read back its own object after another identity wrote to it, and its own marker was gone.
VERIDOR-SESSION-001 warning on reference A logged-in identity's request drew an authentication status — what a dead session looks like from the runner's side.
VERIDOR-CACHE-001 warning on reference An intermediary announced the response as served from a cache, so the marker scan describes the object at some earlier time.

A consumer that enumerates tool.driver.rules to build a rule inventory sees four entries on a clean run, not nine. A rule missing from a document means no result referenced it — never that the condition was checked and not found.

VERIDOR-VACUOUS-001 is note, never warning, and that is deliberate. It is a fact about your declaration, not a finding about your target, and code scanning renders warning and error as things wrong with the code under test. Before it existed, the console said "1 expectation(s) accept both access and denial, so they assert nothing about the boundary" into a run-level property that GitHub code scanning does not surface — so in the recommended CI setup, a check that asserted nothing was invisible.

The tool's fullDescription and VERIDOR-AUTHZ-001's carry the scope sentence with them; the other rules do not yet:

Veridor evaluates the expectations you declared. A finding means observed behaviour differed from your declaration; it does not establish that the declaration is correct.

SARIF outlives the console output that produced it. It lands in a UI where the reader never sees the run summary, so the limit travels with the rule.

What each result carries

{
  "ruleId": "VERIDOR-AUTHZ-001",
  "level": "error",
  "message": {"text": "Identity 'other_tenant' received HTTP 200 (declared: one of [404]) and the response carried 'invoice_owner''s marker: ..."},
  "locations": [
    {
      "physicalLocation": {
        "artifactLocation": {"uri": "fixtures/veridor.demo.json"},
        "region": {"startLine": 48}
      }
    }
  ],
  "partialFingerprints": {"veridorFindingId/v1": "0ec4c5cbd79deaed6e9f"},
  "properties": {
    "checkId": "invoice-tenant-isolation",
    "checkName": "Invoice tenant isolation",
    "identity": "other_tenant",
    "verdict": "vulnerable",
    "evidenceGrade": "confirmed",
    "runId": "20260805T024017Z-185dc9",
    "markerState": "present"
  }
}
Property What it is
checkId The check's id in your configuration.
checkName What the check claims, alongside what it is called. A --promote stub still says TODO: describe the boundary you are asserting here, and that sentence reaching a code-scanning UI is the difference between a reviewed assertion and a machine hypothesis presented as one. Falls back to the id for a sequence finding.
identity The identity whose probe produced this result.
verdict vulnerable, unexpected_denial, or inconclusive.
evidenceGrade confirmed, suspected, ungraded, or n/a.
runId The run identifier; automationDetails.id is veridor/ followed by it.
markerState Present only when a marker was configured for this check.
stepIndex Present only on a sequence finding.

Read verdicts and evidence grades before you triage on evidenceGrade. confirmed is proof of data flow, never proof of policy.

runs[0].properties.interpretation carries the same block the run document does — what Veridor measured, what a finding means, what a pass does not mean, and what confirmed establishes — so a downstream consumer inherits the caveat instead of re-deriving it. When a run leaned on an identity marker_probe rather than a same-check control, runs[0].properties.oracleDisclosures names the exclusions that were licensed that way.

Findings anchor to the declaration they graded

The uri is your configuration file, not a source file in your application. Veridor grades a declaration; the declaration is what it can point at.

startLine is the exact line of the expectation that produced the finding. In the example above, line 48 of fixtures/veridor.demo.json is the "other_tenant": key inside that check's expect block — not the check, not the file's opening brace. A reviewer reading the pull-request diff sees the annotation on the declaration they would have to change.

Two exceptions, stated because a line number that is silently wrong is worse than one that is honest:

  • A run document written by an older build carries no line, and a result converted from it falls back to line 1.
  • VERIDOR-VACUOUS-001 results anchor to line 1 of the configuration. They describe an expectation that asserts nothing; they are not graded against a probe, and they do not carry a verdict or an evidenceGrade.

The line is computed at run time and stored in the run document, because veridor report --from-run run.json may run on a machine where the configuration has since moved.

What partialFingerprints buys

partialFingerprints.veridorFindingId/v1 is Veridor's own finding id, there so that code scanning can track the same finding across runs instead of closing and reopening it. GitHub documents that it uses partial fingerprints to match results between uploads, and falls back to location without one, where a reformatted config file would look like a whole new set of findings. That is GitHub's documented behaviour; no upload has been observed to see it.

The id is a stable digest over the check id, the identity, the finding kind, the method, the path, the request-body digest, and the expected statuses. It is deterministic across runs and across machines, which is also what makes the regression diff — new, resolved, unchanged, escalated — mean anything.

Verdicts and grades ride outside the fingerprint

verdict, evidenceGrade and markerState are result properties and are not part of the id. Adding markers to an existing suite never invalidates your baseline, and refining this vocabulary never reports your whole suite as new. That is a design commitment: a Veridor upgrade must not turn every finding into a new one.

It also means the fingerprint alone cannot tell you a finding got worse. The diff reports that separately as escalated: unchanged since the baseline, but the grade rose to confirmed.

Related: exit codes, reports, the GitHub Action, what a finding does and does not mean.