SBOM for embedded firmware: generating, checking and actually using it
An SBOM, or software bill of materials, is the exhaustive list of a product's software components with their versions and licences. On a typical embedded firmware that means between 200 and 800 entries, the overwhelming majority of which come from code you did not write. The Cyber Resilience Act makes it part of the technical file, but its real value lies elsewhere: without it you cannot answer the only question that matters when a CVE lands, namely "does this affect me".
The two formats, and which to choose
SPDX (ISO/IEC 5962) comes from the licence compliance world, under the Linux Foundation. It is verbose, very precise about licences and package relationships, and it is the format that embedded Linux build systems produce natively.
CycloneDX, from OWASP, comes from the application security world. It is more compact, oriented towards vulnerability analysis, and handles service dependencies and hardware components better.
In practice the choice rarely arises: you produce whatever your build system can generate, and convert if a customer demands the other. Both are accepted by authorities, and analysis tools read both.
What matters more than the format is having PURL or CPE identifiers on every component. Without them, automatic matching against vulnerability databases does not work, and your SBOM becomes an archive document rather than a tool.
Generating from Yocto
Yocto produces an SPDX SBOM natively, and on current releases the create-spdx class is inherited by default through INHERIT_DISTRO: there is nothing to enable. On an older release, or if your distro changed INHERIT_DISTRO, add it explicitly:
INHERIT += "create-spdx"
Two variables are worth knowing: SPDX_PRETTY for readable JSON, and SPDX_INCLUDE_SOURCES to include source files in the document.
The main output is an IMAGE-MACHINE.spdx.json file in tmp/deploy/images/MACHINE/, with additional files in tmp/deploy/spdx.
Two things to know. The SBOM describes what the build produced, not what is actually installed in the final image: a package that was compiled but not installed can still appear. And source archiving, through archiver, is a separate mechanism that must be enabled on its own if you want to rebuild identically in five years.
Generating from Buildroot
Buildroot offers make legal-info, which produces licenses.txt, a manifest and the source tree. That is not an SBOM in the strict sense: no standard format, no PURL identifiers, therefore no automatic CVE matching.
To get a usable SPDX or CycloneDX you need an external tool that scans the result, such as syft on the generated filesystem:
syft dir:output/target -o spdx-json > sbom.spdx.json
syft dir:output/target -o cyclonedx-json > sbom.cdx.json
This scan-the-filesystem approach has a structural weakness: it sees binaries, not provenance. A library statically linked into an executable disappears from the inventory. That is the flaw of any after-the-fact generation, and the reason an SBOM produced by the build beats one produced from the finished product.
The Zephyr case
Zephyr generates SPDX through its build tooling:
# in prj.conf, or on the command line
CONFIG_BUILD_OUTPUT_META=y
west build -b <board> <app> -d build
west spdx -d build # SPDX 2.3 by default
west spdx -d build --spdx-version 3.0 # for SPDX 3.0
The --init option, still found in many tutorials, is deprecated and will be removed in Zephyr 5.0: a build with CONFIG_BUILD_OUTPUT_META now requests the required information itself.
The output separates three documents: the application, the SDK, and the Zephyr kernel with its modules. That separation is useful, because Zephyr CVEs are published per module rather than for the whole tree.
Watch out for the classic gap: the Zephyr SBOM covers what is compiled into the image, but not the bootloader. If you use MCUboot you must generate and attach its own SBOM, otherwise a bootloader vulnerability will go unnoticed.
The CVE link, where an SBOM starts paying off
Producing the document achieves nothing without the loop that consumes it. The pattern that works:
- the build produces the SBOM on every run, tagged with the firmware version
- the SBOM is archived and versioned alongside the released artefact
- a scanner compares it daily against vulnerability databases
- every match triggers an applicability assessment
Step 4 is the one that gets skipped, and it is the expensive one. A CVE match is not a vulnerability: the affected code may not be compiled in, the function may not be reachable, the configuration may be out of scope. That is what VEX is for, a format that lets you formally state "this component is present, this CVE does not apply, here is why".
Without VEX, a team tracking three hundred components receives enough irrelevant alerts to stop reading them within two months.
The traps that make an SBOM worthless
| Trap | Consequence |
|---|---|
| Generated once, by hand | Stale from the next build onwards, therefore wrong |
| No PURL or CPE | No automatic matching possible |
| Produced by scanning the final binary | Static libraries vanish |
| No exact version, such as "1.x" | Impossible to decide whether the CVE applies |
| Bootloader missing | A whole class of vulnerabilities stays invisible |
| Not archived with the release | No way to know what shipped in the 2025 firmware |
The last point carries the most weight. The support obligation runs for five years minimum: you must be able to state, in 2031, exactly what was inside the firmware shipped in 2026. An SBOM that is not archived with the artefact is useless on the day it would have been useful.
What to put in place, in order
Enable generation inside the build system, not beside it. Check that every entry carries an exact version and a PURL or CPE identifier. Archive the SBOM with the signed binary, in the same artefact repository. Point a vulnerability scanner at it. And adopt VEX from the first wave of alerts, before the team learns to ignore them.
References
- Yocto Project, Creating a Software Bill of Materials,
create-spdxinherited by default and output locations - Zephyr Project, west spdx, commands and the deprecation of
--init - Yocto Project, SPDX support
- Linux Foundation, SPDX, ISO/IEC 5962
- OWASP, CycloneDX
- CISA, Minimum Requirements for VEX
Going further
These mechanisms make sense once you have produced an SBOM on a real project, seen what is missing, and worked one CVE match through end to end. That is what our Cyber Resilience Act course and our embedded Linux and secure development courses cover.