Software bill of materials
The components a build says it ships, read from the bill of materials it already wrote, whether that is CycloneDX or SPDX, JSON, XML or tag-value, normalized into one list carrying the licence each component names.
| Facts key | sbom |
| Version | v1 |
| Script | sbom.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 bills of materials to read. | bom.json,bom.xml,*.cdx.json,*.cdx.xml,cyclonedx*.json,cyclonedx*.xml,sbom*.json,*.spdx.json,sbom*.spdx | GUARDRAIL_INPUT_REPORTS |
maxComponents | Most components to carry in the facts. The counts stay whole when components are dropped. | 500 | GUARDRAIL_INPUT_MAXCOMPONENTS |
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": ["sbom"]
}sbom = guardrail.facts("sbom")Facts
The document sbom 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 bill of materials was read, none when there was none. |
reason | Why nothing was read, present only when source is none. |
reports | Every bill of materials that was read, in path order. |
reports[].path | Path of the bill of materials, relative to the directory the CLI runs in. |
reports[].modified | When it was last written, ISO 8601, so a bill of materials older than the build is recognisable. |
reports[].format | Format it was read as: cyclonedx or spdx. |
format | Format of the first bill of materials read, cyclonedx or spdx. Each report carries its own in reports[].format. |
specVersion | Specification version that first bill of materials declares, such as 1.5 or SPDX-2.3, or null when it declares none. |
counts.total | Components across every bill of materials that was read. |
counts.licensed | Components naming at least one licence. |
counts.unlicensed | Components naming none. |
licenses | Licence identifier to the number of components naming it. A component naming two licences counts under both. |
components | The components themselves, up to maxComponents. |
components[].name | Name of the component, as the bill of materials names it. |
components[].version | Version of the component, empty when it carries none. |
components[].purl | Package URL of the component, or null when it carries none. |
components[].type | What the component is, such as library, application or container, or null. |
components[].licenses | Every licence the component names, by identifier or expression. |
dropped | Components left out because maxComponents 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 sbom collector:
{
"source": "report",
"reports": [
{
"path": "bom.json",
"modified": "2026-03-04T10:15:00Z",
"format": "cyclonedx"
}
],
"format": "cyclonedx",
"specVersion": "1.5",
"counts": {
"total": 4,
"licensed": 3,
"unlicensed": 1
},
"licenses": {
"Apache-2.0": 2,
"MIT": 1
},
"components": [
{
"name": "http4k-core",
"version": "6.57.2.0",
"purl": "pkg:maven/org.http4k/http4k-core@6.57.2.0",
"type": "library",
"licenses": [
"Apache-2.0"
]
},
{
"name": "slf4j-api",
"version": "2.0.13",
"purl": "pkg:maven/org.slf4j/slf4j-api@2.0.13",
"type": "library",
"licenses": [
"MIT"
]
},
{
"name": "guava",
"version": "33.2.0-jre",
"purl": "pkg:maven/com.google.guava/guava@33.2.0-jre",
"type": "library",
"licenses": [
"Apache-2.0"
]
},
{
"name": "logback-classic",
"version": "1.5.6",
"purl": "pkg:maven/ch.qos.logback/logback-classic@1.5.6",
"type": "library",
"licenses": []
}
],
"dropped": 0
}