Generating an SBOM for your stack
The CRA Desk scan reads a software bill of materials (SBOM); it never generates one. Your build produces the inventory, the scan turns it into a Cyber Resilience Act (CRA) readiness report. Below is the generate step for each ecosystem we watch - the scan step after it is the same everywhere.
CRA Desk reads CycloneDX 1.5, 1.6 and 1.7 and SPDX 2.3, JSON only. XML and SPDX tag-value are detected and explained rather than mis-parsed, so a file in the wrong format is reported, never silently treated as an empty inventory.
One generate step per ecosystem
Any stack (cdxgen)
Run in CI:
npx @cyclonedx/cdxgen -o sbom.json .
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
cdxgen auto-detects most stacks, which is also its weakness: a repository holding several will report whatever it recognises, so check the component count matches what you expect.
Generator documentationJavaScript / TypeScript (npm)
Run in CI:
npm ci npm sbom --sbom-format cyclonedx --omit dev > sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
npm sbom needs an installed tree, so npm ci has to run first. Omitting --omit dev puts your build tooling in the inventory and every advisory against it in your report.
Generator documentationJava / Kotlin (Maven)
Add to pom.xml:
<plugin>
<groupId>org.cyclonedx</groupId>
<artifactId>cyclonedx-maven-plugin</artifactId>
<version>2.9.1</version>
<configuration>
<outputFormat>json</outputFormat>
</configuration>
</plugin>Run in CI:
mvn -B org.cyclonedx:cyclonedx-maven-plugin:makeAggregateBom -DoutputFormat=json
- Produces:
- target/bom.json
- Format:
- CycloneDX JSON
makeAggregateBom is the important word: a multi-module build without it writes one bom per module and the scan reads a single file rather than merging them.
Generator documentationJava / Kotlin (Gradle)
Add to build.gradle:
plugins {
id 'org.cyclonedx.bom' version '1.10.0'
}Run in CI:
./gradlew cyclonedxBom
- Produces:
- build/reports/bom.json
- Format:
- CycloneDX JSON
The plugin writes into the module that applies it, so in a multi-project build apply it to the root project or point sbom-path at the module you actually ship.
Generator documentationPython (pip / Poetry)
Run in CI:
pip install -r requirements.txt pip install cyclonedx-bom cyclonedx-py environment -o sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
cyclonedx-py environment describes the installed environment, not requirements.txt, so dependencies have to be installed first. For a lockfile instead, use cyclonedx-py poetry or cyclonedx-py requirements.
Generator documentationGo
Run in CI:
go install github.com/CycloneDX/cyclonedx-gomod/cmd/cyclonedx-gomod@latest cyclonedx-gomod mod -json -output sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
cyclonedx-gomod mod reads go.mod, which lists more than a binary actually links. Use cyclonedx-gomod app for what ships, at the cost of building it first.
Generator documentationRust (Cargo)
Run in CI:
cargo install cargo-cyclonedx cargo cyclonedx --format json
- Produces:
- bom.json
- Format:
- CycloneDX JSON
A workspace writes one bom.json per member crate next to its Cargo.toml, so name the crate you ship rather than assuming a single file at the root.
Generator documentationC# / .NET (NuGet)
Run in CI:
dotnet tool install --global CycloneDX dotnet restore dotnet CycloneDX . -o . --json --filename sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
The tool resolves what restore put on disk, so dotnet restore has to run first; pointing it at a solution rather than a single project is what gets every shipped assembly in.
Generator documentationRuby (Bundler)
Run in CI:
bundle install npx @cyclonedx/cdxgen -t ruby -o sbom.json .
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
Run bundle install first: the inventory is read from the resolved bundle, not from the Gemfile. cdxgen is used here because the older cyclonedx-ruby gem defaults to XML, which the scan refuses.
Generator documentationPHP (Composer)
Run in CI:
composer require --dev cyclonedx/cyclonedx-php-composer composer CycloneDX:make-sbom --output-format=JSON --output-file=sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
Without --output-format=JSON the plugin writes XML, which the scan detects and refuses rather than mis-parsing. Add --omit=dev to leave development packages out.
Generator documentationElixir / Erlang (Mix)
Add to mix.exs:
defp deps do
[{:sbom, "~> 0.6", only: :dev, runtime: false}]
endRun in CI:
mix deps.get mix sbom.cyclonedx -f -o sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
The default output is XML, so -f json is not optional here. The dependency is dev-only, which keeps it out of your release.
Generator documentationContainer image (Alpine / apk)
Run in CI:
docker build -t app:ci . curl -sSfL https://get.anchore.io/syft | sh -s -- -b /usr/local/bin syft app:ci -o cyclonedx-json=sbom.json
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
syft scans the built image, not the checkout - pointed at the repository it produces an SBOM with no apk packages in it at all. Scan the same tag you ship: in a multi-stage build only the final stage's packages are in the inventory, which is the point of scanning the image rather than the build.
Generator documentationTraps worth knowing before your first run
- sbom-path takes a comma-separated list and one * in the final segment (build/*.cdx.json). It reads the first match and never merges several files, so a build writing one SBOM per module needs an aggregating command rather than a wildcard.
- Several generators emit XML by default, or XML alongside JSON. The scan refuses XML deliberately, so point sbom-path at the .json file and pass the format flag where the tool has one.
- SPDX has to be JSON (--output spdx-json in syft). SPDX tag-value files are detected and explained rather than read.
- On GitLab, Dependency Scanning already produces CycloneDX: point sbom_path at gl-sbom-*.cdx.json instead of adding a generator of your own.
A generator that runs is not the same as coverage. Components with no purl, no version, or from an ecosystem outside the ones we watch are counted as gaps in every report rather than reported as clean.