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¶
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:
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-001results anchor to line1of the configuration. They describe an expectation that asserts nothing; they are not graded against a probe, and they do not carry averdictor anevidenceGrade.
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.