How-to · By Chris · 07 May 2026 · 6 min read
Last reviewed 07 May 2026
Generate a CRA-compliant SBOM in 15 minutes
The EU Cyber Resilience Act effectively requires every software product on the EU market to ship with a software bill of materials. The tutorial I wish I'd had when I started: pick a format, generate it for your stack, validate it, and wire it into CI, in about 15 minutes.
The EU Cyber Resilience Act doesn’t say “you must produce an SBOM” in so many words. But Annex I, Section 2 makes one effectively mandatory by requiring you to “identify and document vulnerabilities and components … by drawing up a software bill of materials in a commonly used and machine-readable format”. For most products this lands on 11 December 2027 for full compliance, but the 11 September 2026 vulnerability-reporting deadline arrives sooner, and you need an SBOM to know what to report.
When I first sat down to do this for a client product (a dull mid-sized B2B Node + Python stack), I spent half a day reading vendor whitepapers before realising the actual generator commands take less than a minute each. So this is the tutorial version of that day. Pick a format, generate, validate, ship.
Where the SBOM fits in the compliance bundle
An SBOM isn’t a standalone deliverable. It’s a piece of connective tissue that several other CRA artefacts depend on or reference. Generating one feeds at least seven downstream things:
| CRA artefact | How the SBOM is used |
|---|---|
| Risk assessment | Lists the components actually evaluated; without it, the risk analysis can’t be reproduced |
| Technical documentation (Annex VII) | The SBOM is part of the bundle. Annex VII point 1(c) requires component identification |
| Vulnerability handling process (Art. 13) | Defines the monitoring scope. You can only track CVEs against components you’ve enumerated |
| Conformity assessment (Art. 24) | Notified bodies (Modules B / C / H) inspect the SBOM as part of the Class I and Class II review |
| Declaration of Conformity | References the technical documentation, which contains the SBOM |
| Article 14 vulnerability reports | Affected components are identified by their SBOM coordinates (purl + version) |
| Customer / procurement requests | Increasingly required as a release artefact regardless of the CRA |
The practical lesson I learned from doing this twice: generate the SBOM once and let every other artefact reference it. Don’t maintain a separate component list in the technical documentation, another in the risk assessment, and a third in your vulnerability handling docs. They will drift, and a notified body audit will find the inconsistencies (mine had a frontend dep listed as v1 in the tech doc and v3 in the SBOM, embarrassingly). The SBOM is the source of truth; the rest is metadata pointing at it.
Where the SBOM physically lives:
- At build time: in your CI artefacts, attached to the release
- In the tech doc bundle: included as a referenced annex
- Published with the product: increasingly expected in a
.well-known/location, in release assets, or in the manufacturer portal once that’s live in 2027
Step 1 — Pick a format (1 minute)
Two formats are accepted under the CRA: CycloneDX (OWASP) and SPDX (Linux Foundation).
| If you… | Use |
|---|---|
| Are starting from zero and care most about CRA + vulnerability tracking | CycloneDX |
| Already produce SPDX for a US federal contract or licensing programme | SPDX (don’t switch) |
| Need both for different audiences | Generate CycloneDX, convert to SPDX with cyclonedx-cli when needed |
The rest of this tutorial uses CycloneDX. The same flow with SPDX is almost identical; every tool below has an SPDX twin.
Step 2 — Generate the SBOM (5 minutes)
Pick the snippet that matches your stack. Each generator runs in seconds and writes a bom.json you can open in any text editor.
Node.js / npm
npx --yes @cyclonedx/cyclonedx-npm \
--output-file bom.json \
--output-format JSON
Run this from a directory with a package-lock.json. Without the lockfile you’ll get top-level deps only. The CRA accepts that as a minimum, but full transitive is the norm.
Python / pip
pip install cyclonedx-bom
cyclonedx-py environment --output-file bom.json
For Poetry projects, swap environment for poetry. For requirements files, requirements -r requirements.txt.
Go
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest
cyclonedx-gomod app -json -output bom.json
mod instead of app if you’re producing a library SBOM rather than a binary.
Java / Maven
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.8.0</version>
</plugin>
Then mvn cyclonedx:makeAggregateBom. For Gradle, the plugin is org.cyclonedx.bom.
.NET
dotnet tool install --global CycloneDX
dotnet CycloneDX YourProject.csproj -j -o .
Container images (any language)
syft your-image:tag -o cyclonedx-json > bom.json
Syft introspects an image’s layers and detects components across multiple ecosystems in one pass. Useful when your product is a container, and the only realistic option for a multi-language image.
Step 3 — Validate it (2 minutes)
A malformed SBOM is worse than no SBOM in a notified-body audit. Validate before you ship:
npx --yes @cyclonedx/cyclonedx-cli validate \
--input-file bom.json \
--fail-on-errors
Open the JSON in the CycloneDX Web Tool to eyeball it. Three things to check:
- Every component has a purl (a string starting with
pkg:). This is what makes your SBOM machine-matchable to vulnerability databases. - Every component has a version. Pinned versions, not ranges like
^4.0. - The dependency graph is populated. If
dependenciesis empty or missing, your tool ran in flat mode. Re-run with the lockfile present.
The first time I generated one of these, I missed item 3 entirely. The output looked fine in the browser but Grype found nothing because the dep graph was empty. Spent an hour debugging Grype before realising the SBOM itself was the problem.
Step 4 — Scan it for vulnerabilities (3 minutes)
The whole point of the SBOM is that it lets you check for known vulnerabilities continuously. Wire it to a scanner:
# Grype reads CycloneDX directly
grype sbom:bom.json
# Or Trivy
trivy sbom bom.json
Both tools cross-reference your components against the GitHub Advisory Database, OSV, and the NVD. Output looks like:
NAME INSTALLED FIXED-IN VULNERABILITY SEVERITY
lodash 4.17.20 4.17.21 CVE-2021-23337 High
Under Article 14, high-severity exploitable vulnerabilities trigger a 24h notification to ENISA. The SBOM scan is what tells you you have one.
Step 5 — Wire it into CI (4 minutes)
Generate on every release at minimum, every push to main ideally. A GitHub Actions snippet:
- name: Generate SBOM
run: npx --yes @cyclonedx/cyclonedx-npm --output-file bom.json
- name: Scan SBOM
uses: anchore/scan-action@v3
with:
sbom: bom.json
fail-build: false # set to true when you're ready
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: bom.json
Archive the SBOM alongside your release artefacts. ENISA’s draft guidance increasingly expects you to publish the SBOM with the product, either in a .well-known/ location, in the release assets, or via the protected manufacturer portal once that goes live in 2027.
What to do next
- Add VEX assertions to your SBOM to declare that known CVEs in your dependencies don’t affect your product. This is the difference between “we have 47 high-severity issues” and “we’ve assessed 47 issues and 3 are exploitable in our usage”. CycloneDX has VEX baked in. See the official docs.
- Sign the SBOM with Sigstore or in-toto so its provenance is verifiable. For Class II products this is effectively required.
- For the regulatory backdrop and field-by-field SBOM requirements, see the Compliance guide → SBOM section.
TL;DR
# the entire 15-minute exercise, for an npm project
npx @cyclonedx/cyclonedx-npm --output-file bom.json --output-format JSON
npx @cyclonedx/cyclonedx-cli validate --input-file bom.json --fail-on-errors
grype sbom:bom.json
If those three commands run clean, you have the foundation of a CRA-compliant SBOM workflow. Everything else (VEX, attestation, transitive depth, automated reporting) is incremental on top.