Generating an SBOM for Any stack (cdxgen)
The step below writes a CycloneDX JSON software bill of materials (SBOM) in your own pipeline. The CRA Desk scan reads that file and turns it into a Cyber Resilience Act (CRA) readiness report - it never generates one for you.
Run in CI:
npx @cyclonedx/cdxgen -o sbom.json .
- Produces:
- sbom.json
- Format:
- CycloneDX JSON
What the generator actually reads
cdxgen walks the repository and fires one detector per language it recognises, so what lands in the file depends on which manifests and lockfiles are present the moment it runs. A bare checkout gives it declared dependencies; a checkout after the build gives it resolved ones. Restrict it with -t when the repository also contains something you do not ship.
Versions and package URLs
Because several detectors can fire in a single run, one file can carry pkg:npm, pkg:maven and pkg:pypi components side by side - that is the point of this recipe and also its risk. A detector that found only a manifest emits ranges rather than pinned versions and a component with no resolved version cannot be matched against an advisory.
The whole CI job
GitHub Actions
- uses: actions/checkout@v4
- run: npx @cyclonedx/cdxgen -o sbom.json .
- uses: mmalinowski/cradesk-action@v1
with:
sbom-path: sbom.jsonGitLab CI/CD
generate-sbom:
script:
- npx @cyclonedx/cdxgen -o sbom.json .
include:
- component: gitlab.com/cradesk/scan/readiness@1
inputs:
sbom_path: sbom.jsonOnly the first step changes with your stack - the CRA Desk step after it is identical everywhere.
What goes wrong here
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.
Repositories with more than one thing in them
A monorepo is where this recipe earns its place: one run covers every language in it and one file carries them all. The cost is that the file then describes the repository rather than a product - if you ship one service out of six, the readiness report has to say which components belong to it.
What CRA Desk does with the result
cdxgen reports whatever stacks it recognises, so coverage follows the package URLs it emits: components landing in one of the watched ecosystems are matched against OSV and the rest are counted as gaps rather than reported as clean.
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.