Skip to content

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.
  • PATH may not be -: run prints its own summary on stdout. junit=- is refused by the argument parser with exit 2.
  • Report paths get the same checks as --json and --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 filesystem VERIDOR.JSON is refused where veridor.json would be.
  • Each report's directory must also be writable before any request: run creates it if needed and writes and removes a test file in it. If it cannot, it refuses with exit 2 and Nothing 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 into 2, unless the gate already failed on a finding (1), which is kept. So an exit 0 always 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 (--input is the older spelling) or --from-drift DRIFT.json; exactly one of them.
  • --format is required, unless you use the older --sarif PATH spelling, which still works: veridor report --input run.json --sarif veridor.sarif.
  • --output -, or no --output at all, writes to stdout with nothing else on it. A file output prints Wrote <Label> report to PATH.
  • Refused with exit 2: a missing or doubled input, a missing --format, --sarif mixed with --format or --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), a schema_version this Veridor does not read, and a run in which a finding, a container, or a field inside one that a format reads (a probe's response, a finding's evidence.request, a gate reason's finding_ids or sequences, ...) 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 exit 70, "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"}]
}
  • reasons lists every condition that moved the exit code under these flags, in precedence order. The first one's exit_code is the process exit code, and reasons is empty exactly when that code is 0. Veridor's run-document validator enforces that invariant.
  • not_enforced lists what was present but not gated under these flags, each with the flag (enforced_by) that would gate it.
  • The same reasons list is the GitHub Action's gate-reasons output.

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:

veridor report --from-drift drift.json --format markdown

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.