How to get the SBOM of your Linux firmware: Yocto, Buildroot, or an image you didn't build
The CRA asks for a machine-readable inventory of components. How to produce it depending on how your firmware is built, what your build system already gives you, and why scanning the image is not enough.
Annex I of the Cyber Resilience Act requires manufacturers to identify and document the components of their product, including by drawing up a software bill of materials (SBOM) in a commonly used, machine-readable format covering at least the top-level dependencies (Part II, point 1). You don’t have to publish it: it is part of the technical documentation (Annex VII), and the market surveillance authority can ask for it with a reasoned request.
Without an SBOM you can’t answer the most basic question of vulnerability handling: when a new CVE comes out, does it affect me? How you get one depends on how you build your firmware. There are three cases.
Case 1: you build with Yocto
Good news: you are probably generating it already. The Yocto reference distribution (Poky) inherits the create-spdx class by default, and every image build leaves the SBOM in tmp/deploy/images/<machine>/, with per-recipe files in tmp/deploy/spdx/.
What changes is the SPDX version:
- Scarthgap (5.0, LTS supported until April 2028):
create-spdxis SPDX 2.2. SPDX 3.0 is available but has to be enabled. - From Styhead (5.1) on:
create-spdxis SPDX 3.0.
If you are on scarthgap and want SPDX 3.0, which records vulnerability status best, add this to local.conf or your distro configuration:
INHERIT_DISTRO:remove = "create-spdx"
INHERIT_DISTRO:append = " create-spdx-3.0"
If your distro doesn’t derive from Poky (some BSPs don’t), check that the class is active:
bitbake -e core-image-minimal | grep -E '^(INHERIT|INHERIT_DISTRO)='
What the Yocto SBOM knows and a scanner doesn’t. Yocto applies security patches without changing the package version: a package with three CVEs fixed by patches still declares the same version as without them. The build knows which patches each recipe carries, because they are named after the CVE or have a CVE: CVE-YYYY-NNNN line in their header, and writes that into the SBOM:
- In SPDX 2.2, patched CVEs go into a text field of each package.
- In SPDX 3.0, they go in as VEX relationships: “fixed”, “not affected” (with a justification) or “affected”. By default (
SPDX_INCLUDE_VEX = "current") it includes those not already fixed in the upstream version.
CVEs you have assessed and dismissed are declared with CVE_STATUS in the recipe, with a reason, and in SPDX 3.0 they also end up in the SBOM as “not affected”. That is exactly the kind of evidence the CRA expects: not “zero CVEs”, but “these are not exploitable, and here is why”.
What it doesn’t cover. The Yocto SBOM describes recipes. If a recipe builds a Go or Rust binary that bundles its own dependencies (Docker and containerd are the typical example), those internal libraries don’t show up as components and don’t get Yocto’s patches. For those you need to look at the image, as in case 3.
Case 2: you build with Buildroot
Buildroot doesn’t install a package database on the device, so the inventory has to come from the build:
make show-infodumps the packages of your configuration as JSON, with version, licence, CPE and patches.- From Buildroot 2025.02,
utils/generate-cyclonedxturns that into a CycloneDX SBOM:
make show-info | utils/generate-cyclonedx > sbom.cdx.json
CVEs you have marked with <PACKAGE>_IGNORE_CVES in the .mk file appear in the SBOM as dismissed. On an older release, make show-info exists since 2019.05 and the script only uses the Python standard library: you can try it on your tree, or convert the JSON with your own script. The older the release, the less show-info contains (the CPE, for example, only appears from 2021.02).
To match it against vulnerabilities, make pkg-stats queries the NVD database and lists CVEs per package.
make legal-info is something else: it collects licences and sources for GPL compliance. Useful, but not an SBOM.
Case 3: you have the image, but not the build
This happens more than you’d think: the firmware was built by the module vendor, by an integrator who is long gone, or it’s a Debian-style distribution nobody rebuilds. The CRA doesn’t let you off: the manufacturer is responsible for the third-party components it integrates and must exercise due diligence on them.
The first step is to ask the vendor for the SBOM. If they have a Yocto or Buildroot build, they have it or can generate it in minutes.
If not, you can reconstruct it from the image with Syft:
syft dir:./rootfs -o spdx-json > sbom.spdx.json
It works well in these cases:
- Images with a package manager Syft understands: Debian and Ubuntu (dpkg), RPM-based images and Alpine.
- Go binaries: they embed the list of modules they were built with, and Syft reads it. That’s how Docker’s internal dependencies, or your own services’, show up.
- Well-known binaries: Syft identifies the version of a catalogue of common programs (BusyBox, OpenSSL and others) even without a package database.
And it has limits worth knowing:
- Syft doesn’t read the opkg (ipk) package database, the format of many Yocto images. And Buildroot leaves none. In those cases you get a partial inventory: whatever it recognises from the binaries.
- It knows nothing about patches. It sees a package version and assigns all of its CVEs, even if the manufacturer has fixed them. The result is a list inflated with false positives.
- It identifies the kernel by version, without its configuration. To know which kernel CVEs really affect you, you need the
.config, because many are in code your kernel doesn’t build. IfCONFIG_IKCONFIG_PROCis enabled, it’s in/proc/config.gzon the device.
What we recommend
- Generate the SBOM in the build, not by hand and not once: for every release you ship, stored next to the image. The CRA requires handling vulnerabilities throughout the support period, and for that you need the inventory of every release in the field.
- Cross-check the build SBOM with a scan of the image. The first knows about patches; the second sees what the build doesn’t declare (binaries’ internal dependencies, files added by hand). The differences between the two are a finding in themselves.
- Document your dismissals with
CVE_STATUSin Yocto or_IGNORE_CVESin Buildroot, always with the reason. That is your VEX, and it’s what you’ll show when someone asks. - Keep the kernel
.configof every release. Without it, the list of kernel CVEs can’t be filtered.
Our Quick Scan does exactly that cross-check: you send us the image (and the Yocto SBOM if you have it), and within 48 business hours you get the inventory, the vulnerabilities that really apply to your device, justified dismissals and what’s missing for the CRA. If you’d rather talk it through for your case first, book 30 minutes.
Does your product have to comply with the CRA?
Start with a €1,900 Quick Scan or book 30 minutes to talk it through.