CRA Desk

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 documentation

JavaScript / 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 documentation

Java / 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 documentation

Java / 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 documentation

Python (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 documentation

Go

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 documentation

Rust (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 documentation

C# / .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 documentation

Ruby (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 documentation

PHP (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 documentation

Elixir / Erlang (Mix)

Add to mix.exs:

defp deps do
  [{:sbom, "~> 0.6", only: :dev, runtime: false}]
end

Run 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 documentation

Container 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 documentation

Traps 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.