Security scan findings
Findings from whatever scanner the build already ran, read from the report it left behind and normalized into one shape, whether the tool was a SAST, an SCA, a secret detector or an infrastructure scanner.
| Facts key | scan |
| Version | v1 |
| Script | scan.py |
| Timeout | 60 seconds |
Inputs
| Input | Description | Default | Environment |
|---|---|---|---|
reports | Comma separated globs matched against every file in the working directory and against bare file names, naming the scanner reports to read. | *.sarif,*.sarif.json,trivy*.json,grype*.json,osv*.json,semgrep*.json,snyk*.json | GUARDRAIL_INPUT_REPORTS |
maxFindings | Most findings to carry in the facts. The counts stay whole when findings are dropped. | 200 | GUARDRAIL_INPUT_MAXFINDINGS |
A guardrail that declares an input of the same name passes it through, so the guardrail's configuration in buildnote.json is what decides these values.
A guardrail asks for these facts by name, and reads them back the same way:
{
"collect": ["scan"]
}scan = guardrail.facts("scan")Facts
The document scan collects. A path carrying [] is an entry of the list before it, and one carrying [path] is a key of the object before it.
| Fact | Meaning |
|---|---|
source | report when a scanner report was read, none when there was none. |
reason | Why nothing was read, present only when source is none. |
reports | Every report that was read, newest path order. |
reports[].path | Path of the report, relative to the directory the CLI runs in. |
reports[].modified | When the report was last written, ISO 8601, so a stale report is recognisable. |
reports[].format | Format it was read as: sarif, trivy, grype or osv. |
reports[].producer.name | Scanner that wrote it, as the report names itself, or null when it names none. |
reports[].producer.version | Version of that scanner, or null when the report carries none. |
reports[].findings | How many findings that report carried. |
counts.total | Findings across every report that was read. |
counts.critical | Findings the scanner rated critical. |
counts.high | Findings rated high. |
counts.medium | Findings rated medium. |
counts.low | Findings rated low. |
counts.info | Findings rated informational. |
counts.unknown | Findings the scanner gave no severity, or one nobody recognises. |
kinds.sast | Findings about the repository's own code. |
kinds.sca | Findings about a dependency. |
kinds.secret | Findings about a credential in the tree. |
kinds.iac | Findings about infrastructure code. |
kinds.container | Findings about a container image. |
kinds.unknown | Findings nothing classified. |
findings | The findings themselves, up to maxFindings. |
findings[].id | Rule or vulnerability identifier the scanner gave it. |
findings[].severity | critical, high, medium, low, info or unknown. |
findings[].message | What the scanner said, truncated to 500 characters. |
findings[].path | File it was found in, relative to the working directory, or null. |
findings[].line | Line it was found on, or null. |
findings[].kind | sast, sca, secret, iac, container or unknown. |
findings[].package.name | Dependency it is about, present only for a finding about one. |
findings[].package.version | Version of that dependency. |
findings[].identifiers | Every CVE, GHSA or other identifier the scanner attached. |
findings[].fixedIn | Version that fixes it, or null when the scanner names none. |
findings[].fingerprint | Stable identity of the finding across runs, so the same finding is recognisable in a later build. |
dropped | Findings left out because maxFindings was reached. |
A collector that cannot finish prints what it had along with an incomplete key saying why, so the facts below the point it stopped are absent. A check that cares reads incomplete before the rest.
Example facts
The example facts collected by scan collector:
{
"source": "report",
"reports": [
{
"path": "security-scan.sarif",
"modified": "2026-03-04T10:15:00Z",
"format": "sarif",
"producer": {
"name": "Semgrep OSS",
"version": "1.86.0"
},
"findings": 1
},
{
"path": "trivy.json",
"modified": "2026-03-04T10:15:00Z",
"format": "trivy",
"producer": {
"name": "trivy",
"version": null
},
"findings": 1
}
],
"counts": {
"critical": 1,
"high": 1,
"medium": 0,
"low": 0,
"info": 0,
"unknown": 0,
"total": 2
},
"kinds": {
"sast": 1,
"sca": 1,
"secret": 0,
"iac": 0,
"container": 0,
"unknown": 0
},
"findings": [
{
"id": "kotlin.lang.security.insecure-hostname-verifier",
"severity": "high",
"message": "Hostname verification is disabled, so any certificate is accepted.",
"path": "service/src/main/kotlin/Queue.kt",
"line": 1,
"kind": "sast",
"package": null,
"identifiers": [],
"fixedIn": null,
"fingerprint": "262b267f2df80c2e"
},
{
"id": "CVE-2021-44228",
"severity": "critical",
"message": "log4j-core: remote code execution via JNDI lookup",
"path": "service/build.gradle.kts",
"line": null,
"kind": "sca",
"package": {
"name": "org.apache.logging.log4j:log4j-core",
"version": "2.14.1"
},
"identifiers": [
"CVE-2021-44228"
],
"fixedIn": "2.15.0",
"fingerprint": "c1d7cd4ef6d2fed9"
}
],
"dropped": 0
}