sbom-embedded
Generate a CycloneDX SBOM from a Yocto or Buildroot build you have already run, by reading the manifest files the build wrote — no rebuild, no bitbake, under a tenth of a second.
The EU Cyber Resilience Act (2024/2847) requires device manufacturers to keep a machine-readable component list for their products. Syft, Trivy and cdxgen are good at containers and npm and weak at embedded Linux build systems. This fills that gap.
Install and run
Without installing anything permanently:
pipx run sbom-embedded ./output > sbom.json
uvx sbom-embedded ./output > sbom.json
Or install it:
pipx install sbom-embedded
To run an unreleased change straight from the repository, point the same commands at git instead:
pipx run --spec git+https://github.com/RchrdWrd/sbom-embedded sbom-embedded ./output
uvx --from git+https://github.com/RchrdWrd/sbom-embedded sbom-embedded ./output
Python 3.11 or newer, tested on 3.11 through 3.14. The tool only reads files, so it runs anywhere Python does.
Usage
Point it at a build directory. It works out which build system produced it — you do not have to say.
sbom-embedded ./build/tmp/deploy > sbom.json # Yocto
sbom-embedded ./output > sbom.json # Buildroot
| Option | Meaning |
|---|---|
--format |
Output format. cyclonedx is the only one. |
--image |
Which image to describe, when a Yocto deploy holds several. |
--name |
Name for the product. Defaults to the image name, or buildroot. |
--product-version |
Version of the product. Omitted from the SBOM if not given. |
--output, -o |
Write to a file instead of stdout. |
Real output
Run against a Buildroot manifest in this repository:
$ sbom-embedded tests/fixtures/buildroot-2023.02
{
"components": [
{
"bom-ref": "pkg:generic/busybox@1.36.1",
"licenses": [
{
"license": {
"name": "GPL-2.0, bzip2-1.0.4"
}
}
],
"name": "busybox",
"purl": "pkg:generic/busybox@1.36.1",
"type": "library",
"version": "1.36.1"
},
...
If a Yocto deploy directory holds more than one image, it stops and lists them rather than picking one:
$ sbom-embedded tests/fixtures/yocto-6.0.2
error: tests/fixtures/yocto-6.0.2 holds several images (core-image-full-cmdline,
core-image-minimal); pick one with --image
On a real Buildroot build
Verified end to end on Buildroot 2026.08-rc3-28-g79fd6241e4:
git clone https://gitlab.com/buildroot.org/buildroot.git
cd buildroot
make qemu_x86_64_defconfig
make legal-info # downloads sources, does not compile: 10-30 min
sbom-embedded ./output -o sbom.json
$ sbom-embedded ./output -o sbom.json
wrote 44 components to sbom.json
44 components in 0.09 s; all 44 carry a purl, a version and a license; 26
distinct license expressions; the document validates against the CycloneDX 1.6
schema. That build's manifest is committed as
tests/fixtures/buildroot-2026.08/ — see
PROVENANCE.md.
Ubuntu 25.10 and newer:
make legal-inforefuses to start with "You have an uutils 'install' version installed", because those releases ship uutils coreutils rather than GNU coreutils. You can fix it system-wide withsudo update-alternatives --install /usr/bin/install install /usr/bin/gnuinstall 100, or just for one build without touching the system:mkdir -p /tmp/gnushim && ln -sf /usr/bin/gnuinstall /tmp/gnushim/install PATH=/tmp/gnushim:$PATH make legal-info
Read this before you scan the output
A vulnerability scan of this SBOM can report zero findings while the firmware is full of known vulnerabilities. This is not a hypothetical.
Every component gets a pkg:generic/<name>@<version> purl. Neither Yocto nor
Buildroot packages exist in any ecosystem repository, so there is no better
purl type available. But Grype, Trivy and Dependency-Track do not resolve
pkg:generic to a vulnerability namespace — they reach the NVD through CPEs,
which this tool does not emit, because a CPE would be a guess about vendor and
product strings rather than something any manifest records.
Measured, not assumed. The SBOM from the real Buildroot build above, scanned with Grype 0.118.0:
$ grype sbom:sbom.json
No vulnerabilities found
The same five packages, with CPEs added by hand purely to demonstrate the difference:
NAME INSTALLED TYPE VULNERABILITY SEVERITY EPSS RISK
busybox 1.38.0 UnknownPackage CVE-2026-38754 High 0.4% (33rd) 0.3
busybox 1.38.0 UnknownPackage CVE-2026-38755 High 0.3% (27th) 0.2
busybox 1.38.0 UnknownPackage CVE-2026-38753 High 0.2% (15th) 0.2
Same document, same packages, same scanner. The difference is the identifier, not the firmware.
So: use this SBOM as a component inventory and a compliance record. Do not read a clean Grype run on it as evidence that the firmware is clean. For vulnerability matching you need a tool that maps package names to CPEs, or a scanner configured for these package names specifically.
How much of this is verified on real builds
Both paths have been walked from a build to an SBOM on real hardware, not from fixtures alone.
Buildroot. A real make legal-info on Buildroot 2026.08-rc3 produced the
44-package manifest shown above, and a real make produced a complete output
tree of 867,355 files. That tree turned out to contain ten files named
*.manifest — all of them Windows application manifests inside host package
sources (host-python3, host-cmake, host-ninja, gcc). None sits at
images/<dir>/*.manifest, which is the only reason Yocto detection does not
fire on a Buildroot tree; there is a test pinning that.
Yocto. bitbake core-image-minimal was run to completion for qemux86-64
on the Yocto 6.0.2 release revisions, and the tool was run against the
tmp/deploy directory it wrote:
$ sbom-embedded ./deploy -o sbom.json
wrote 39 components to sbom.json
39 components in 0.09 s, every one with a purl, a license and a yocto:recipe
property, validating against the CycloneDX 1.6 schema. That deploy tree is
committed as tests/fixtures/yocto-6.0.2-live/.
The same image was then rebuilt with PACKAGE_CLASSES = "package_ipk". That
matters because every other fixture here comes from an rpm-backend build, and
rpm is the one backend whose version column carries no package revision. In
the ipk manifest all 38 rows do (busybox 1.37.0-r0), two carry -r1, and
netbase all 1:6.5-r0 carries an epoch. The tool strips all of that, so the
37 purls the two images share are byte-identical: the same firmware
described the same way regardless of how it was packaged. Both manifests are
committed, and a test compares them.
It is the only fixture holding both manifest kinds from a single build, which
makes it the one that demonstrates why they must never be joined by package
name: the image manifest lists 37 packages, the license manifest 39, and 11 of
the 37 names have no counterpart on the other side — libc6 against glibc,
libz1 against zlib, libcrypto3 against libcrypto, and so on.
Building Yocto on a current host: the 6.0.2 release works, but the 5.0.9 release does not — its bitbake crashes on Python 3.14, and its
UNINATIVE_MAXGLIBCVERSIONis below a current glibc. ConfiguringSSTATE_MIRRORSagainstsstate.yoctoproject.orgis what makes the build practical: 373 of 396 wanted objects came from the mirror, so it fit on a machine with 7 GB of free disk instead of needing 20-40.
Known limitations
- Yocto builds without a license manifest produce no licenses. The image
manifest has no license column. If your build kept
tmp/deploy/licenses/, that is read instead and you get licenses and recipe names; otherwise you get names, versions and purls only. Buildroot always carries licenses. - Package names are not upstream project names. Your firmware contains
libcrypto,openssl-confandopenssl-ossl-module-legacy; a CVE database knowsopenssl. Where a Yocto license manifest is read, the recipe behind each package is recorded as ayocto:recipeproperty. Where only the image manifest exists, the names are the Debian-renamed forms (libc6,libz1) with no way back. - License strings are copied, not normalised. Yocto writes
GPL-2.0-only & MITand BuildrootGPL-2.0+ (programs), LGPL-2.1+— neither is a valid SPDX expression. Valid SPDX identifiers and expressions are emitted as such; everything else is emitted as a named license, verbatim. Nothing is guessed at or dropped. - No supplier and no hashes. Neither build system records a supplier or a per-package hash, so those fields are absent rather than invented.
- The dependency graph is flat. Every package hangs off the image. The manifests read do not carry inter-package dependencies.
- Buildroot
SOURCE ARCHIVE,SOURCE SITEandLICENSE FILESare not emitted. - The product name and version are yours to supply. A Buildroot manifest
carries no product identity, so the root component is named
buildrootunless you pass--name, and has no version unless you pass--product-version. Nothing is invented to fill them.
The underlying reasoning, and the manifest formats in detail, are in DESIGN.md.
Development
uv sync
uv run pytest
Without uv:
python3 -m venv .venv
.venv/bin/pip install -e . --group dev # needs pip 25.1+ for --group
.venv/bin/python -m pytest
92 tests, a second or two. They run from fixture files under
tests/fixtures, never from a live build. Every fixture is unmodified output
from a real Yocto or Buildroot build —
PROVENANCE.md records where each came from and
what it is kept for. The format details matter too much to mock.
How to contribute, and how a release is cut, are in CONTRIBUTING.md.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sbom_embedded-0.1.1.tar.gz.
File metadata
- Download URL: sbom_embedded-0.1.1.tar.gz
- Upload date:
- Size: 58.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a9afcc0a8250f7340de71363bc806aa8cb8f79902f457e534ebf01b255f2cdf5
|
|
| MD5 |
364640e4c093cb30cb79184fe9606d7c
|
|
| BLAKE2b-256 |
ded40deaf5d50095d6cb991826078207ecdb99b9cd6c5f822a708ce4148d92f1
|
Provenance
The following attestation bundles were made for sbom_embedded-0.1.1.tar.gz:
Publisher:
publish.yml on RchrdWrd/sbom-embedded
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sbom_embedded-0.1.1.tar.gz -
Subject digest:
a9afcc0a8250f7340de71363bc806aa8cb8f79902f457e534ebf01b255f2cdf5 - Sigstore transparency entry: 2725052001
- Sigstore integration time:
-
Permalink:
RchrdWrd/sbom-embedded@f6a36ff45977879a8de2162d9e995fe85678aba6 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/RchrdWrd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f6a36ff45977879a8de2162d9e995fe85678aba6 -
Trigger Event:
push
-
Statement type:
File details
Details for the file sbom_embedded-0.1.1-py3-none-any.whl.
File metadata
- Download URL: sbom_embedded-0.1.1-py3-none-any.whl
- Upload date:
- Size: 21.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4a4986c6796a8c41f0919ff9cac0968037da088fa196859b7f663247e65067c7
|
|
| MD5 |
c3343d820a37b42168588bef0d2583a2
|
|
| BLAKE2b-256 |
ce79267ef81e666a7c27946bce5e2558f9d5dc6d62326272b63dc68c28513e0c
|
Provenance
The following attestation bundles were made for sbom_embedded-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on RchrdWrd/sbom-embedded
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sbom_embedded-0.1.1-py3-none-any.whl -
Subject digest:
4a4986c6796a8c41f0919ff9cac0968037da088fa196859b7f663247e65067c7 - Sigstore transparency entry: 2725052536
- Sigstore integration time:
-
Permalink:
RchrdWrd/sbom-embedded@f6a36ff45977879a8de2162d9e995fe85678aba6 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/RchrdWrd
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@f6a36ff45977879a8de2162d9e995fe85678aba6 -
Trigger Event:
push
-
Statement type: