
A working guide to generating CycloneDX SBOMs with cdxgen across Node, Python, Java, Go, C/C++, and containers, and what to do beyond first SBOM generation.
Note: This guide was fact-checked in mid-July 2026 against the cdxgen releases current at that time. cdxgen is actively developed, so later releases may add features, fix bugs, or change behavior. Check the official cdxgen documentation for the current flags and commands before you rely on any of the examples below.
OWASP cdxgen is the fastest way to turn a codebase into a CycloneDX SBOM. It is the official generator in the CycloneDX ecosystem.
cdxgen covers most modern languages, and produces a valid SBOM in one command. If your goal today is "get me an SBOM," this is the tool you can rely on.
This guide helps you get your first SBOM, include optional details into it and operationalize SBOM generation in pipelines. Then it draws a line most SBOM content skips. Generating the file is one step. Using it afterward is another. Knowing where one ends and the other begins saves you a bad assumption later.
Installing cdxgen
Three ways to install cdxgen -
One requirement catches people out. C, C++, and Python need Java 21 or newer on the machine. cdxgen hangs on Java 8 or 11. Install a current JDK and set JAVA_HOME first, or the run stalls for no obvious reason.
Generate first SBOM
Move into the project directory and run one command.
That produces a CycloneDX file, bom.json. cdxgen auto-detects the project type and usually gets it right. When it misses, the output is thin or wrong. Name the type explicitly with -t from day one.
For a repo with several services, scan recursively. cdxgen assembles one aggregate BOM.
Set the spec version on purpose. The version you emit is the version customers and regulators hold you to.
Ecosystem coverage
cdxgen wins on breadth. One tool replaces a drawer full of language-specific plugins. The common searches:
Python:
cdxgen -t python. Reads requirements files, Poetry, and PDM. Needs Java 21.Java / Maven / Gradle:
cdxgen -t java. Resolves the full tree, not just direct declarations.Node / npm:
cdxgen -t javascript. Walks the lockfile for the real transitive graph.Go:
cdxgen -t go.C / C++:
cdxgen -t c. Java 21 required. Best results come from a real build, not a bare source tree.Containers:
cdxgen -t docker alpine:3.20 -o bom.json. Pulls OS packages and language dependencies from the image.
Wiring this into CI? Check your ecosystem against the Supported Project Types list first. Coverage is wide, not universal. Better to find a gap before a pipeline depends on it.
Including evidence of component use
A basic SBOM lists what your manifests declare. That is a claim. Evidence is proof. For a regulated submission, that difference matters.
The --evidence flag (cdxgen 9.9 and later) records where each component is used, not just what a manifest declared. Go further with call-stack evidence. It shows whether a vulnerable function is reachable from your code.
Reachability earns its keep. Most scanners flag every vulnerable dependency. Many sit in code paths you never call. Call-stack evidence shows which findings touch your code. That is the difference between a triage queue of four hundred and one of twelve.
Signing SBOM
An unsigned SBOM is a text file anyone can edit. Going to a customer or regulator? Sign it.
That generates a key pair and attaches a signature. Downstream, cdx-verify confirms the file is unchanged since signing. In production, use your own managed keys, not a generated test pair. The mechanism is built in. Turn it on early.
Building in CI: GitHub Actions, Azure DevOps
GitHub Actions
None of this helps if it runs on a laptop once. Make generation a build step. An SBOM should come out of every build, like a test report.
A minimal GitHub Actions job that generates the SBOM and keeps it as an artifact:
Two things earn their place. Pinning Java 21 with setup-java prevents the silent hang on older JDKs. Uploading bom.json as an artifact keeps the file after the run. That is the whole point of generating it in CI.
Pushing to a Dependency-Track server? cdxgen submits directly. Set the URL and API key as repository secrets.
Azure Pipelines
The same job in Azure Pipelines. Same shape: set up Java 21, install cdxgen, generate, publish the file so it survives the agent.
For a containerized build, swap the install-and-run lines for the Docker image and mount the workspace. That skips the Node and Java setup.
Cdxgen SBOM pitfalls
cdxgen produces a file for almost anything. Whether that file is complete depends on the ecosystem, and on whether the project is built when you scan it. This bites teams who assume every SBOM carries the same depth.
The biggest lever is the build lifecycle. By default, cdxgen generates at the build phase for applications. Depth for several ecosystems depends on the project being restored or built first. Pre-build scans are faster and safer in CI. They can also miss the full transitive tree. Know which mode you are in.
Ecosystem | Transitive depth | What it needs | Watch out for |
|---|---|---|---|
Java (Maven) | Full | Resolvable POM; Maven is the default over Gradle | Force Gradle with |
JavaScript / Node | Full | A lockfile present | No lockfile means direct-only, a shallow BOM |
Python | Good | Java 21 installed |
|
Go | Good | Module-aware project | Binary-only needs |
.NET | Full only after restore |
| Container image bundles SDK 8.0; a version mismatch drops the tree |
C / C++ | Partial | A real build context, Java 21 | A bare source tree yields a thin BOM; build first |
Rust, Ruby, PHP | Good | Standard manifests and lockfiles | Coverage is solid; verify against your project |
Habits that keep the BOM complete:
Always pass
-t. Auto-detection usually works. When it guesses wrong, the BOM is quietly incomplete. Naming the type removes the guess.Build or restore before you scan for .NET, C/C++, and anything needing the full tree. A pre-build scan is not wrong. It is shallower. Know which one you shipped.
Check for a lockfile. For Node and similar, no lockfile means direct dependencies only. A direct-only BOM passes an audit while hiding most of your supply chain.
Validate the output. cdxgen validates against the schema by default. Keep it on. Treat a warning as a signal to look, not a line to skip.
Pin the spec version. Make
--spec-versiona deliberate choice. The version you emit is the version a customer or regulator checks against.
None of these are reasons to avoid cdxgen. They are the difference between a BOM that looks done and one that is.
C/C++ and embedded: the hardest case
In the pitfalls table, C/C++ is partial because in those ecosystems a generic generator, cdxgen included, has the least to work with. And the gap between what a tool reports and what actually ships is the widest in the industry.
The specific traps:
No dependency manifest to start from. Node has a lockfile, Python has one, Rust and Go have one. C/C++ has nothing equivalent. Conan, vcpkg, and CMake
FetchContenthelp when they are used, but most embedded projects build with plain Makefiles, IAR, or STM32CubeIDE, so there is no machine-readable graph to parse.Vendored source disappears into your tree. Libraries like lwIP, mbedTLS, or nlohmann/json get copied straight into the project, and once copied they look like first-party code. A generator that only reads manifests never sees them. Detecting them takes fingerprinting against a corpus of known releases, and once the code is patched, exact matching fails and you are into fuzzy matching with real uncertainty.
Static
.aarchives are near black boxes. Alibfoo.abuilt months ago from vendored source carries a filename and little else. No supplier, no upstream version, none of the NTIA minimum elements. Linker-linspection gives you a name at best, and embedded builds often compile the RTOS, HAL, and crypto from source with no-lflags at all.Inventorying the vendor folder overcounts. A shared
vendor/directory holds many libraries, but any single build target uses only some of them. Listing the whole folder puts components in the SBOM that are not in the binary, which turns into false positives in every downstream vulnerability scan.--gc-sectionsmakes this worse: code that compiles but is never called contributes zero bytes to the image, yet a naive scan still reports it.Silicon-vendor SDKs add noise. ST, NXP, TI, and Renesas ship forked, renamed, adapted copies of FreeRTOS, lwIP, and mbedTLS inside their SDKs. Fingerprinting these against upstream releases is unreliable without vendor-specific detection.
An accurate embedded C/C++ SBOM has to come from what the build actually compiles and links for a specific target, correlated with vendored-code detection and platform awareness. That details can only come from build-time analysis, not manifest parsing, and it is outside of what cdxgen is built to do.
This is the problem lynkctl, Interlynk's SBOM generator for embedded C/C++, is designed around: toolchain-aware extraction from GNU Make, CMake, and IAR, paired with a curated index of embedded open-source components for the vendored layer. If your target is firmware, start there rather than forcing a general-purpose generator to cover a case it was never meant to. Our full breakdown of why this is hard is in The State of SBOM Generation for C/C++.
Beyond SBOM generation
One more thing decides whether your SBOM program holds up.
cdxgen is a generator. It looks at one project, one image, or one build, and produces an accurate CycloneDX file. It does that well. Every command above is real. For a single codebase, you now have what you need.
A generator does not manage what it generates. That is not a knock on cdxgen. It is the category. Move from one project to a portfolio and a different set of problems shows up. None of them are generation problems.
You are not producing one SBOM. You are producing thousands, one per build per service per release. They pile up faster than anyone reads them.
Different teams emit different formats and versions. The same component gets named three ways. Nothing aggregates cleanly across products.
Every SBOM is a snapshot. Accurate at build time, blind to the CVE disclosed next week. A file on disk does not re-check itself.
Suppliers send SBOMs too, in whatever shape they please. Those have to be validated and folded in with your own.
Generating the file passes the audit. Answering "where are we exposed, across everything we ship, right now" is a different system. You find out which one you built the first time a critical CVE lands on a Friday.
So use cdxgen. It is the right tool for the step it owns. Just size the step correctly. Generation is the start of an SBOM program, not the whole of one.
Summary
Install cdxgen. Run cdxgen -t <type> --spec-version 1.6 -o bom.json. Add --evidence for proof, and --with-reachables to cut vulnerability noise. Sign what leaves the building. Wire it into GitHub Actions or Azure Pipelines so it runs on every build.
Watch the depth pitfalls. Pass -t explicitly. Build or restore before scanning the ecosystems that need it. Confirm a lockfile is present, or you ship a direct-only BOM.
That gives you accurate, current CycloneDX files. Turning a stack of files into an answer you can trust under pressure takes the layer that sits on top. Plan for that before you need it, not after.
Interlynk gives security and compliance teams one system of record for every SBOM, from cdxgen or any other generator, internal or supplier, with normalized component identity and continuous monitoring built in. Keep every output current as your dependencies and the disclosures evolve, and answer the portfolio-wide exposure question in minutes instead of days. Trusted by security and compliance teams at 100+ regulated companies.