Skip to main content

sbom-embedded

CI Python License: MIT

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

pipx run --spec git+https://github.com/RchrdWrd/sbom-embedded sbom-embedded ./output > sbom.json

With uv:

uvx --from git+https://github.com/RchrdWrd/sbom-embedded sbom-embedded ./output > sbom.json

Or install it properly:

pipx install git+https://github.com/RchrdWrd/sbom-embedded

Python 3.11 or newer. 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-info refuses 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 with sudo 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_MAXGLIBCVERSION is below a current glibc. Configuring SSTATE_MIRRORS against sstate.yoctoproject.org is 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-conf and openssl-ossl-module-legacy; a CVE database knows openssl. Where a Yocto license manifest is read, the recipe behind each package is recorded as a yocto:recipe property. 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 & MIT and Buildroot GPL-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 SITE and LICENSE FILES are not emitted.
  • The product name and version are yours to supply. A Buildroot manifest carries no product identity, so the root component is named buildroot unless 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.

Releasing

Publishing runs from .github/workflows/publish.yml through PyPI Trusted Publishing, so there is no API token anywhere in the repository or in GitHub secrets — PyPI verifies the workflow's identity over OpenID Connect.

One-time setup, on PyPI under Your projects → Publishing → Add a pending publisher:

Field Value
PyPI project name sbom-embedded
Owner RchrdWrd
Repository name sbom-embedded
Workflow name publish.yml
Environment name pypi

Repeat it on test.pypi.org with environment testpypi. Then, in the repository's Settings → Environments, create both environments — add a required reviewer on pypi if you want a manual gate.

To release:

  1. Run the Publish workflow manually against testpypi and check the rendered project page.
  2. Bump version in pyproject.toml, update CHANGELOG.md, commit.
  3. Tag and push: git tag -a v0.2.0 -m "..." && git push origin v0.2.0.

The tag push publishes to PyPI. The workflow refuses to publish if the tag and the packaged version disagree, and it installs the built wheel and generates an SBOM from it before either upload step runs. A PyPI release cannot be replaced, only yanked, which is why TestPyPI comes first.

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.

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

sbom_embedded-0.1.0.tar.gz (58.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sbom_embedded-0.1.0-py3-none-any.whl (21.6 kB view details)

Uploaded Python 3

File details

Details for the file sbom_embedded-0.1.0.tar.gz.

File metadata

  • Download URL: sbom_embedded-0.1.0.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

Hashes for sbom_embedded-0.1.0.tar.gz
Algorithm Hash digest
SHA256 bc0126aca64b21a015b3704314fe245ccd4dba4ec76053df1c822c4a343f8902
MD5 aac19b68d008e82bffb1362a78b9837d
BLAKE2b-256 4833661ebad0199548d8f9b611068041945dee717d2bcd9b7cd65553167e009f

See more details on using hashes here.

Provenance

The following attestation bundles were made for sbom_embedded-0.1.0.tar.gz:

Publisher: publish.yml on RchrdWrd/sbom-embedded

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file sbom_embedded-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: sbom_embedded-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 21.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for sbom_embedded-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 b30c5a0966a45b99391e5d8b7f03e2700252036a86f8f5171e2fb5fc61951bc3
MD5 8f0da2823c284ab8ac04e1e75457f896
BLAKE2b-256 8b36a8a8cefe4b5885ae70b5b2273c83b6176907ff6c2590b325c8ef47f54cab

See more details on using hashes here.

Provenance

The following attestation bundles were made for sbom_embedded-0.1.0-py3-none-any.whl:

Publisher: publish.yml on RchrdWrd/sbom-embedded

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.2.0

2 files

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page