Reports¶
veridor run exits with a code a pipeline can read. The reports on this page
are how the same decision reaches a test tab, a security dashboard or a merge
request. Every format is rendered from the run document alone, and every one of
them reads the gate object veridor run writes into that document, so a job
that exited 3 because nothing was answered never renders as a pass.
--format |
What it is | Where it goes |
|---|---|---|
sarif |
SARIF 2.1.0 | GitHub code scanning (upload-sarif), Jenkins Warnings NG |
junit |
JUnit XML | GitLab reports: junit, Jenkins junit, Azure PublishTestResults@2, CodeBuild JUNITXML |
gitlab-sast |
GitLab security report, schema 15.2.5 | GitLab reports: sast |
codequality |
GitLab Code Quality JSON array | GitLab reports: codequality |
markdown |
A Markdown summary | $GITHUB_STEP_SUMMARY, a merge request note, a build artifact |
Writing them from the run¶
veridor run --config veridor.json \
--report junit=veridor-junit.xml \
--report gitlab-sast=gl-sast-report.json \
--report markdown=summary.md
--report FORMAT=PATH is repeatable. run prints Wrote <format> report to
<PATH> for each one.
- Reports are written whatever the gate decides, because a failing gate is when CI needs them.
PATHmay not be-:runprints its own summary on stdout.junit=-is refused by the argument parser with exit2.- Report paths get the same checks as
--jsonand--sarif, before any request: they must be distinct, and may not overwrite the configuration, the baseline, the specification or the managed run history. Paths are compared as files, so on a case-insensitive filesystemVERIDOR.JSONis refused whereveridor.jsonwould be. - Each report's directory must also be writable before any request:
runcreates it if needed and writes and removes a test file in it. If it cannot, it refuses with exit2andNothing has been sent, naming the flag, the path and the uid:gid (the full message is on the configuration reference). - A report that still cannot be written after the probes is named on stderr
(
veridor: could not write the <fmt> report to <PATH>: ...) and turns the exit code into2, unless the gate already failed on a finding (1), which is kept. So an exit0always comes with its reports.
Measured on the demo target, re-run for this page on an earlier tree (before 38025ed): a
vulnerable run with all five --report flags exited 1 and printed five
Wrote ... report lines.
What a report never carries¶
Header values are never written, query values are redacted in every stored
URL, and every value Veridor expanded from a ${NAME}, wherever it sat (a
path segment, a ;param, the base URL, a body), appears in every report and
run document as ${NAME}, never as the value. A value shorter than 8
characters is the exception, because it cannot be told apart from ordinary
text; the command names each such ${NAME} on stderr instead. A credential
typed literally into a configuration or a flag is your own text and is written
as typed. Measured for this page on an earlier tree (before 38025ed): with a 19-character
${VERIDOR_SEG} in a check's path, the listener received the value on the
wire, and the run document, the GitLab SAST report and both managed history
files carried ${VERIDOR_SEG} and never the value (the console output of that
run did not print the path at all, so it tested nothing there); a 7-character
${VERIDOR_SHORT} was named on stderr (expanded to fewer than 8 characters,
so Veridor did not replace the value). report --from-run renders what the
run document holds, so a run document written before this rule may still hold
values. Safety posture has the rule in
full.
Rendering them later¶
From a run document you already have, without repeating a single request:
veridor report --from-run run.json --format junit --output veridor-junit.xml
veridor report --from-run run.json --format markdown >> "$GITHUB_STEP_SUMMARY"
--from-run RUN.json(--inputis the older spelling) or--from-drift DRIFT.json; exactly one of them.--formatis required, unless you use the older--sarif PATHspelling, which still works:veridor report --input run.json --sarif veridor.sarif.--output -, or no--outputat all, writes to stdout with nothing else on it. A file output printsWrote <Label> report to PATH.- Refused with exit
2: a missing or doubled input, a missing--format,--sarifmixed with--formator--output, an output equal to the input (--output must not overwrite the input run) or the configuration, a file that is not a Veridor run (a matrix, a drift or coverage document, an empty object), aschema_versionthis Veridor does not read, and a run in which a finding, a container, or a field inside one that a format reads (a probe'sresponse, a finding'sevidence.request, a gate reason'sfinding_idsorsequences, ...) has the wrong type or is missing. The message names the field. A hand-edited run with a wrong type one level down used to crash a renderer with exit70, "a bug in Veridor".
For $GITHUB_STEP_SUMMARY, append with >> as above. run --report
markdown=$GITHUB_STEP_SUMMARY would atomically replace the file instead,
erasing what earlier steps wrote.
The gate object¶
veridor run writes the decision it exited with into the run document (in
--json, latest.json and runs/<id>.json). From a measured run against an
unreachable target:
"gate": {
"fail_on": "new+confirmed",
"require_complete": false,
"require_clean_teardown": false,
"exit_code": 3,
"reasons": [{"code": "nothing_answered", "exit_code": 3, "count": 6}],
"not_enforced": [{"code": "inconclusive", "exit_code": 0, "count": 6,
"enforced_by": "--require-complete"}]
}
reasonslists every condition that moved the exit code under these flags, in precedence order. The first one'sexit_codeis the process exit code, andreasonsis empty exactly when that code is0. Veridor's run-document validator enforces that invariant.not_enforcedlists what was present but not gated under these flags, each with the flag (enforced_by) that would gate it.- The same
reasonslist is the GitHub Action'sgate-reasonsoutput.
Reason codes¶
In precedence order:
| Code | Exit | Gated when | Extra keys |
|---|---|---|---|
findings |
1 | the --fail-on policy selects a vulnerable or unexpected_denial finding |
count, by_grade, finding_ids |
coverage_not_measured |
2 | --openapi was given and boundary coverage could not be measured |
|
nothing_answered |
3 | not one probe was answered and nothing about the target was found (any policy but none) |
count |
identity_unauthenticated |
3 | a session identity failed to log in, for a reason that is not transient (any policy but none) |
identities |
teardown_unclean |
3 | --require-clean-teardown and a sequence teardown failed, was incomplete or aborted |
sequences |
expectations_non_asserting |
3 | --require-complete and an expectation accepts both access and denial |
count |
inconclusive |
3 | --require-complete and a probe or step came back inconclusive |
count |
liveness_unproven |
3 | --require-complete (and a policy other than none) and a denial-only expectation did not establish who was denied: a fabricated credential drew the same denial, or none could be sent and nothing else proved the identity live |
identities |
Measured on the demo target, the same unreachable configuration three ways:
| Flags | Exit | reasons |
not_enforced |
|---|---|---|---|
| defaults | 3 |
nothing_answered |
inconclusive |
--fail-on none |
0 |
none | nothing_answered, inconclusive |
--fail-on none --require-complete |
3 |
inconclusive |
nothing_answered |
The three rows were re-measured for this page on an earlier tree (before 38025ed) with the same
results. findings, nothing_answered and inconclusive were produced by
live runs for this page. identity_unauthenticated and teardown_unclean are covered by
synthetic documents in Veridor's test suite and have not been produced by a
live run.
A run document written without veridor run, or by a Veridor older than the
gate object, has no recorded decision. Each format then re-derives one with
the CLI defaults and marks it recorded: false; the job may have used other
flags, and its own exit code decides.
How each format shows the gate¶
| Format | A finding | A non-zero exit that is not a finding |
|---|---|---|
junit |
a <failure> on the probe's or the sequence step's test case; inconclusive and a step that never ran are <error>, never <skipped> |
a <failure type="exit-N"> on the veridor.gate test case, which fails on every non-zero exit and lists every reason |
gitlab-sast |
vulnerable is severity High, unexpected_denial is Info; nothing is Critical. inconclusive is not a vulnerability |
scan.status: failure, with a scan.messages entry per reason. A run that answered nothing is failure under any policy |
codequality |
vulnerable+confirmed is critical, other vulnerable major, unexpected_denial minor, inconclusive info; a non-asserting expectation is an info issue |
one veridor.gate issue, severity major |
markdown |
a findings table, gating findings first | the verdict line: Gate incomplete (exit 3) or Gate failed (exit 2), never passed |
sarif |
a result per finding | not represented: SARIF has no gate |
JUnit¶
One test case per check probe (<check> as <identity>), one per sequence step
(<seq> step <n> (<role>) as <identity>), and veridor.gate. Suite properties
carry veridor.gate.exit_code, veridor.gate.fail_on, veridor.gate.recorded,
veridor.gate.reasons, and the declaration and pass scope sentences as
veridor.scope and veridor.pass_scope. Characters XML 1.0 cannot carry are
replaced with U+FFFD. Measured, and re-run for this page on an earlier tree (before 38025ed): the
vulnerable demo run wrote 7 test cases with 2 failures, the leaking probe and
veridor.gate.
GitLab SAST¶
version is 15.2.5; the scanner and analyzer id is veridor, with the
running Veridor's version. Vulnerability ids are UUIDs derived from Veridor's
finding fingerprint only, so a finding keeps its id across pipelines. The
location is the configuration line the finding graded.
Measured for this page on an earlier tree (before 38025ed): the vulnerable and the unreachable
demo runs' reports validated against GitLab's sast-report-format.json
(self.version 15.2.5) with jsonschema, 0 errors each, with scan.status
success (exit 1, a finding) and failure (exit 3) respectively. A copy
with scan.status changed and version removed, as a control, gave 2
errors. GitLab's ingestion, merge
request widget and vulnerability tracking were not observed. GitLab's SAST
documentation lists merge-request findings and vulnerability management on the
Ultimate tier only.
Code Quality¶
A JSON array of issues with check_name, description, categories,
severity, location and a fingerprint. Veridor's test suite checks it for
unique fingerprints and valid severities only. It is not validated against a
GitLab schema, and its rendering in GitLab was not observed.
Markdown¶
The verdict line, the policy, the counts, every gate reason and every condition
present but not gated, the Scope: and Pass: sentences the console prints,
and the findings. The verdict line starts with one of:
**Gate passed (exit 0).****Gate failed (exit 1):**or**Gate failed (exit 2):****Gate incomplete (exit 3):****Gate exit 0, and nothing was measured**, for a report-only run (--fail-on none) that answered nothing
The report stays under 1 MiB, GitHub's step-summary limit; findings past it are cut at a row with a visible note saying how many.
SARIF¶
See SARIF and code scanning: rule ids, automationDetails,
security-severity and tags, and fingerprints.
Drift¶
A drift report (veridor map --baseline ... --drift drift.json) is an
observation, never a finding, so it renders to Markdown only:
Any other format is refused with exit 2: SARIF, JUnit, GitLab SAST and Code
Quality would all present a change as a finding. The drift document does not
record the --fail-on-drift policy or the exit code, and the summary says so.
It does record measured_in_both_cells, the cells measured in both runs, and
measured_cells for each side. A redirect is not counted as measured, because
Veridor does not follow it; redirect_cells counts the cells in both runs that
were a redirect in at least one. When measured_in_both_cells is 0 the
summary says in bold that not one cell was measured in both runs, and that
under --fail-on-drift Veridor exits 3 rather than 0. Measured for this
page on an earlier tree (before 38025ed): against a baseline recorded while the target was down,
the drift document read measured_in_both_cells: 0, baseline
measured_cells: 0, current measured_cells: 12; the control, a baseline
recorded against the fixed target, read 12, 12, 12. See
drift.
What none of them claims¶
None of these reports is evidence that a boundary is enforced. A finding means observed behaviour differed from your declaration; a passing test case means the target agreed with it for one request.
Related: exit codes, GitLab CI, other CI systems, the GitHub Action.