A firmware chain's SBOM, readable by euvex
euvex reads a CycloneDX SBOM in JSON, or an SPDX SBOM. Yocto writes SPDX by default, Buildroot writes CycloneDX with a script it ships, Zephyr writes SPDX with west spdx. The three paths, then what euvex does with the file, with no promises beyond that.
What euvex reads
CycloneDX JSON 1.2 to 1.6 · SPDX 2.2, 2.3 and 3.0
The format.
CycloneDX in JSON, versions 1.2 to 1.6, whose bomFormat field is CycloneDX. Or SPDX: 2.2 and 2.3 in JSON or tag-value, 3.0 in JSON-LD. The file name hardly matters: euvex recognises the content. Four megabytes at most.
Not read.
CycloneDX in XML, and the .spdx.tar.zst archive from Yocto 5.0. The upload refuses them with a message; it does not convert them.
In SPDX.
The product is the package the document says it describes (in SPDX 3.0, its root element). Every other package becomes a component, with its purl, CPEs, licence and hash; files are not carried over. Several tag-value documents placed end to end are read as one, the first describing the product. What follows about CycloneDX applies to the rest.
The product.
The name and version in metadata.component become those of the scan. If the chain puts its own name there, that is what you will see.
Each component.
Its name (with its group), version, purl, cpe, licences, strongest hash (SHA-512, then SHA-256, then SHA-1) and scope. Nested components are flattened.
The scope.
excluded means “not distributed with the product”: the component leaves the CRA rules, Article 14 detection and the coverage count. required and optional are both counted as distributed, as is a missing scope: that is the cautious reading.
Ignored.
The file's vulnerabilities section, so a VEX or the CVEs your chain marks as fixed. euvex looks for vulnerabilities itself, as of its own date: a CVE fixed by a patch in your chain can therefore appear, and is handled like an alert.
What happens to a firmware's components
purl, CPE, version
On upload, each component is looked up in OSV by its purl. OSV does not index pkg:generic, the identifier these chains write most often, nor a component without a purl. Those components come out “non analysé” (not analysed), and the report does not conclude that there are no vulnerabilities as long as one remains.
During scheduled monitoring, euvex queries the NVD for those components: by their cpe when the SBOM carries one, otherwise by their name and exact version, with an unknown vendor. A component with neither CPE nor version stays “non analysé”, never counted as healthy.
The NVD answers one component at a time, and euvex limits the time it spends on it at each recheck, about thirty components. As long as a component has never been asked about, the scan comes back at every pass, every ten minutes, for the forty-eight hours after the upload: a firmware of three hundred components is covered in about two hours, then rechecked every day. The report shows the “N sur M” (N out of M) coverage in the meantime, and alert emails go out in one go, at the end.
The setting that changes the most
cpe on each component. That is what the NVD understands, and yours is queried rather than a matched name. If your chain can write distribution purls (pkg:deb, pkg:rpm), OSV understands them from the upload. The detail, and what was measured.From Yocto
SPDX 3.0 by default · meta-cyclonedx on 5.0
Yocto writes an SBOM in SPDX format by default, through the create-spdx class that INHERIT_DISTRO inherits.
Yocto 5.1 and later. It is SPDX 3.0, in a single file next to the image, in ${DEPLOY_DIR_IMAGE}, for example core-image-minimal-qemux86-64.rootfs.spdx.json. Upload it as is: the rootfs becomes the product, the packages installed in the image the components, with the CPEs Yocto derives from CVE_PRODUCT. Downloaded sources and recipes are not carried over.
Yocto 5.0 (scarthgap). The default output is a .spdx.tar.zst archive, one document per package, which euvex does not read. The create-spdx-3.0 class is available there as an option from yocto-5.0.14; otherwise, for CycloneDX, you need one more layer: meta-cyclonedx, maintained outside the Yocto Project, with one branch per Yocto version (kirkstone, scarthgap and later).
git clone https://github.com/iris-GmbH/meta-cyclonedx.git
cd meta-cyclonedx
git checkout scarthgapBBLAYERS += "${BSPDIR}/sources/meta-cyclonedx"INHERIT += "cyclonedx-export"
CYCLONEDX_SPEC_VERSION = "1.6"Why set the version. The layer writes 1.4 by default on kirkstone and scarthgap, 1.6 on the later branches, and 1.7 on its main branch. euvex reads 1.2 to 1.6: asking for 1.6 works on all of them.
Where to find the file. It depends on the branch. On scarthgap, bom.json in ${DEPLOY_DIR}/cyclonedx-export; on recent branches, next to the image, with the .cyclonedx.bom.json suffix. Your branch's README is authoritative. Upload the SBOM, not vex.json: euvex does not read VEX.
What it contains. By default, only the packages installed in the image, each with a generic purl derived from CVE_PRODUCT and a CPE. If you include build packages (CYCLONEDX_RUNTIME_PACKAGES_ONLY = "0"), the layer marks them optional, and euvex will count them as distributed. The default setting avoids this noise.
The product name. On the branches that offer them, scarthgap included, CYCLONEDX_IMAGE_NAME and CYCLONEDX_IMAGE_VERSION set the name and version written in metadata.component, and therefore those of the scan.
From Buildroot
utils/generate-cyclonedx · from Buildroot 2025.02
Buildroot ships a script that turns the output of make show-info into CycloneDX 1.6, a version euvex reads as is.
make show-info | utils/generate-cyclonedx \
--project-name thermostat-x --project-version 2.1.0 > sbom.cdx.jsonBefore 2025.08, the two options do not exist, and the product is called buildroot, at the Buildroot version. The script below corrects the name.
make show-info | utils/generate-cyclonedx > sbom.cdx.jsonWhat it contains. Each package with its name, version, licences, and a cpe when the package declares one (LIBFOO_CPE_ID_* variables). No purl: OSV can say nothing about it on upload, the NVD takes over during scheduled monitoring.
Host tools. The file also lists packages built for the build machine, identified by the BR_TYPE property set to host, with no scope. euvex does not read that property: they would be counted as distributed. The following script marks them excluded, which is what CycloneDX says of a component absent at runtime, and sets the product's name and version.
python3 - sbom.cdx.json sbom-euvex.cdx.json thermostat-x 2.1.0 <<'EOF'
import json, sys
source, cible, nom, version = sys.argv[1:5]
with open(source) as f:
bom = json.load(f)
bom["metadata"]["component"]["name"] = nom
bom["metadata"]["component"]["version"] = version
for c in bom.get("components", []):
if any(p.get("name") == "BR_TYPE" and p.get("value") == "host" for p in c.get("properties", [])):
c["scope"] = "excluded"
with open(cible, "w") as f:
json.dump(bom, f, indent=2)
EOFThen upload sbom-euvex.cdx.json. The CVEs Buildroot marks as fixed by its patches, in the vulnerabilities section, are not carried over: see “Ignored”, above.
From Zephyr
west spdx · SPDX 2.3 in tag-value
Zephyr produces its SBOMs with west spdx, after a build made with CONFIG_BUILD_OUTPUT_META. It writes four SPDX 2.3 tag-value documents in BUILD_DIR/spdx/: the application, the Zephyr kernel, the build and the modules.
west build -d build
west spdx -d buildWhat to upload. The three documents that describe the product and what it embeds, placed end to end, the application first: it becomes the product, Zephyr and the modules its components.
cat build/spdx/app.spdx build/spdx/zephyr.spdx build/spdx/modules-deps.spdx > firmware.spdxA module's CPE and purl come from its module.yml (security: external-references): a module that declares none is looked up by its name and version. Zephyr's SPDX 3.0, still experimental, has not been tried.
Uploading and linking
Console · Nouveau scan (new scan)
In the console, “Nouveau scan” (new scan), upload the file. Then link the scan to the product it documents: linking is what opens the RED or Machinery section, the EU declaration and Article 14 detection. Linking a scan to a product.
With each new firmware version, a new SBOM: the product version changes, the product line stays the same.
What this guide does not cover
Where it stops
- euvex does not run your build. SPDX reading was written from the specification and the code of Yocto and Zephyr, and tested on documents of their form, not yet on the output of a real build: report any difference to the contact address.
- meta-cyclonedx, Buildroot's script and west spdx belong to their authors. The commands above are those of their documentation on the review date shown at the bottom of the page; your version's documentation is authoritative.
- A complete SBOM does not prove the product's compliance. It is one part of it, required by the CRA, applicable from 11.12.2027 (Annex I, Part II, point 1).
Sources
Reviewed on 27.10.2026, SPDX on 30.10.2026
- openembedded-core, spdx30_tasks.py, branches walnascar, whinlatter and master, and create-spdx-2.2.bbclass from scarthgap.
- Zephyr, “Software bill of materials: west spdx”, and the generator
scripts/west_commands/zspdx/writer.py. - SPDX 3.0.1 specification and 2.3.
- Yocto Project, “Creating a Software Bill of Materials”, developer manual.
- meta-cyclonedx, READMEs of branches main, kirkstone, scarthgap, whinlatter and wrynose.
- Buildroot manual, section 8.9, 2026.08 edition, and the script utils/generate-cyclonedx at versions 2025.02 to 2026.08.
- CycloneDX 1.6, definition of
scope. - What euvex measures of OSV and the NVD: our sources.
Reviewed against its sources on 30.10.2026
The evidence for the RED directive · The day an Article 14 case opens · All the guides