Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions .github/workflows/build-onie.yml
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,22 @@ jobs:
make MACHINE=kvm_x86_64 shim-self-sign && \
make -j"$(nproc)" MACHINE=kvm_x86_64 all recovery-iso demo'

# Scan the SBOM (produced by `make all`) for known vulnerabilities and
# publish the table to the job log and the run summary. This is a
# report, not a gate: sbom-vuln-scan.py always exits 0 (CVEs already
# fixed by ONIE patches are suppressed via OpenVEX), so it never fails
# the build. The SBOM tools (incl. grype) were provisioned by the SBOM
# step of `make all`.
- name: SBOM vulnerability scan
run: |
docker run --rm \
-v "${PWD}:/onie" \
onie-build-env \
bash -lc 'cd build-config && make MACHINE=kvm_x86_64 sbom-vuln-scan'
for f in build/images/*.sbom.vulns.md; do
[ -f "$f" ] || continue
cat "$f" >> "$GITHUB_STEP_SUMMARY"
done
- name: Upload kvm recovery image
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
Expand Down
7 changes: 7 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ Building ONIE
The recommended way to set up an ONIE build environment is to use a Docker image, as described
in the ONIE Documentation under `Preparing An ONIE Build Environment <https://opencomputeproject.github.io/onie/developers/building.html#preparing-an-onie-build-environment>`_.

Software Bill of Materials
==========================

Every build emits a Software Bill of Materials (CycloneDX 1.6 and SPDX 2.3)
describing the third-party software shipped in the image. See `README.sbom
<README.sbom>`_ for details.

******************************
Mailing List and Collaboration
******************************
Expand Down
128 changes: 128 additions & 0 deletions README.sbom
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
============================================
Software Bill of Materials (SBOM) for ONIE
============================================

Every ONIE image is built with a Software Bill of Materials describing the
third-party software it ships. The SBOM is generated automatically as part of
``make all`` and written next to the image, so a fresh build always produces a
matching SBOM.

What the SBOM describes
=======================

The SBOM lists the third-party software that is **compiled from source and
installed into that machine's image** -- nothing that is merely a build-time
host tool. Concretely, for the machine being built it covers:

- every package whose make fragment installs into the image sysroot
(``$(SYSROOTDIR)``) -- e.g. busybox, e2fsprogs, lvm2, util-linux, dropbear,
... ; and
- the boot / runtime components that are not ordinary sysroot packages: the
Linux kernel, the uClibc-ng C library, the GCC runtime, and the bootloader
(shim / U-Boot).

Build-only tooling (the crosstool-NG cross compiler, binutils, gdb, autoconf,
ncurses, the SBOM tools themselves, ...) is intentionally **excluded** -- it is
not present in the shipped image.

For each component the SBOM records the name, the exact version actually built,
the upstream (canonical, non-mirror) source URL, the source tarball's SHA-256,
the list of ONIE patches applied (as CycloneDX *pedigree*), and the SPDX
license.

Output
======

Two files are written to ``build/images/`` per machine::

<machine-prefix>.sbom.cdx.json CycloneDX 1.6 (source of truth)
<machine-prefix>.sbom.spdx.json SPDX 2.3 (derived via cyclonedx-cli)

CycloneDX 1.6 is the authoritative document; the SPDX 2.3 file is converted
from it. This matches the formats published by the SONiC project.

How it is invoked
=================

The SBOM is part of the default ``all`` target, so an ordinary build emits it::

make -j$(nproc) MACHINE=kvm_x86_64 all

To (re)generate just the SBOM for an already-built image::

make MACHINE=kvm_x86_64 sbom

To remove the SBOM artifacts::

make MACHINE=kvm_x86_64 sbom-clean

The generator is ``build-config/scripts/gen-sbom.py``. It is fully
machine-agnostic: it introspects the build system to discover the enabled
packages and their final versions, and detects licenses from the package
sources, which are already extracted on disk by the time the sysroot is
assembled. There is nothing machine-specific to maintain.

Tooling
=======

``gen-sbom.py`` uses three external tools:

============== ============================================= ==========
Tool Purpose Required?
============== ============================================= ==========
askalono SPDX license detection from package sources for licenses
cyclonedx-cli CycloneDX -> SPDX 2.3 conversion for SPDX output
grype vulnerability scan (optional helper, below) optional
============== ============================================= ==========

If these tools are not already on ``PATH``, the build provisions them
automatically -- pinned to specific versions and verified against hard-coded
SHA-256 hashes -- into a build-local prefix (``build/sbom-tools/bin``). This
requires no root and works in any build environment, so a full-fidelity SBOM is
produced whether you build in the ONIE Docker environment, natively, or in CI.

The provisioning is performed by ``build-config/scripts/install-sbom-tools.sh``,
which you can also run by hand to pre-populate the tools, optionally choosing
the install directory::

# system-wide (needs root)
sudo build-config/scripts/install-sbom-tools.sh

# or into a chosen prefix, no root required
build-config/scripts/install-sbom-tools.sh /path/to/bin

**Offline / air-gapped builds:** the tools are fetched over the network the
first time they are needed. Pre-install them with the script above (while
online) so the build finds them on ``PATH`` and downloads nothing.

**Graceful degradation:** if the tools are unavailable and cannot be
provisioned (e.g. no network), the build still succeeds and still emits a
CycloneDX SBOM -- but license fields fall back to ``NOASSERTION`` and the SPDX
file is skipped. A warning is printed in that case.

License detection and overrides
===============================

Licenses are detected automatically by askalono scanning the
``COPYING`` / ``LICENSE`` / ``COPYRIGHT`` files in each extracted source tree.
A handful of components are genuinely multi-licensed or have no single
top-level license file where automatic detection is ambiguous; those carry a
declared SPDX expression in::

build-config/conf/sbom/license-overrides.json

The override is keyed by make-fragment name and always wins over detection.
Add an entry there if a component reports ``NOASSERTION`` but its license is
known.

Vulnerability scanning (optional)
=================================

An optional helper scans the generated SBOM against the grype vulnerability
database, suppressing CVEs that ONIE has already fixed by patch (recorded as
OpenVEX)::

make MACHINE=kvm_x86_64 sbom-vuln-scan

This writes ``<machine-prefix>.sbom.vulns.json`` to ``build/images/``. It is
not part of ``all`` -- run it explicitly when you want a vulnerability report.
3 changes: 2 additions & 1 deletion build-config/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -508,6 +508,7 @@ endif

include make/images.make
include make/demo.make
include make/sbom.make

# By default do not enable building firmware updates
FIRMWARE_UPDATE_ENABLE ?= no
Expand Down Expand Up @@ -535,7 +536,7 @@ download: $(DOWNLOAD)
$(Q) echo "=== Finished making $@ ==="

PHONY += all
all: $(KERNEL) $(UBOOT) $(SYSROOT) $(IMAGE)
all: $(KERNEL) $(UBOOT) $(SYSROOT) $(IMAGE) $(SBOM_STAMP)
$(Q) echo "=== Finished making onie-$(PLATFORM) $(LSB_RELEASE_TAG) ==="

PHONY += demo
Expand Down
26 changes: 26 additions & 0 deletions build-config/conf/sbom/cpe-overrides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
{
"_comment": [
"Maps an ONIE SBOM component name (lowercased, as emitted by gen-sbom.py) to",
"the NVD CPE 2.3 'part:vendor:product' that grype must match on. ONIE builds",
"from upstream source and carries only pkg:generic PURLs, so grype matches",
"components to CVEs solely via CPE against NVD. The NVD vendor:product rarely",
"equals the package name, so the wrong/absent CPE silently yields a false-clean",
"(zero-finding) scan. Components NOT listed here default to 'a:<name>:<name>'.",
"Mappings were derived from the grype vulnerability DB (highest-CVE canonical",
"vendor:product per product). The CPE version is filled in per build."
],
"linux": "o:linux:linux_kernel",
"util-linux": "a:kernel:util-linux",
"grub": "a:gnu:grub2",
"parted": "a:gnu:parted",
"gcc-runtime": "a:gnu:gcc",
"dropbear": "a:dropbear_ssh_project:dropbear_ssh",
"e2fsprogs": "a:e2fsprogs_project:e2fsprogs",
"uclibc-ng": "a:uclibc-ng_project:uclibc-ng",
"dosfstools": "a:dosfstools_project:dosfstools",
"kexec-tools": "a:kexec-tools_project:kexec-tools",
"shim": "a:redhat:shim",
"lvm2": "a:redhat:lvm2",
"dmidecode": "a:nongnu:dmidecode",
"lzo": "a:oberhumer:lzo2"
}
11 changes: 11 additions & 0 deletions build-config/conf/sbom/license-overrides.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"_comment": "Declared SPDX license (or expression) for components whose source is genuinely multi-license or has no single top-level license file, where automatic detection is ambiguous. Keyed by make-fragment name.",
"e2fsprogs": "GPL-2.0-or-later AND LGPL-2.0-or-later AND BSD-3-Clause AND MIT",
"lvm2": "GPL-2.0-only AND LGPL-2.1-only",
"util-linux": "GPL-2.0-or-later AND LGPL-2.1-or-later AND BSD-3-Clause AND BSD-4-Clause-UC AND MIT",
"gcc-runtime": "GPL-3.0-or-later WITH GCC-exception-3.1",
"uclibc-ng": "LGPL-2.1-or-later",
"dropbear": "MIT",
"kernel": "GPL-2.0-only WITH Linux-syscall-note",
"shim": "BSD-2-Clause-Patent"
}
84 changes: 84 additions & 0 deletions build-config/make/sbom.make
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#-------------------------------------------------------------------------------
#
# Copyright (C) 2026 Brad House <bhouse@nexthop.ai>
#
# SPDX-License-Identifier: GPL-2.0
#
#-------------------------------------------------------------------------------
#
# Generate a Software Bill of Materials (SBOM) for the image.
#
# scripts/gen-sbom.py derives the third-party software COMPILED FROM SOURCE and
# INSTALLED INTO this MACHINE's image (the SYSROOTDIR-installed packages plus the
# kernel / uClibc-ng / GCC runtime / bootloader) from the build system, and emits
# CycloneDX 1.6 (then SPDX 2.3 via cyclonedx-cli). It depends on the assembled
# sysroot so every package source is already extracted (for license detection)
# and every version is final. The SBOM is built as part of `all`, so it is
# always produced alongside the image.
#
# The generator relies on a few external tools (askalono for license
# detection, cyclonedx-cli for the SPDX conversion, grype for the optional
# vulnerability scan). If they are not already on PATH, the build provisions
# them -- pinned + sha256-verified -- into a build-local prefix
# ($(SBOM_TOOLS_DIR)) without needing root, so a full-fidelity SBOM is
# produced in any environment. If provisioning fails (e.g. no network), the
# generator degrades gracefully (NOASSERTION licenses, CycloneDX-only).

SBOM_CDX = $(IMAGEDIR)/$(MACHINE_PREFIX).sbom.cdx.json
SBOM_SPDX = $(IMAGEDIR)/$(MACHINE_PREFIX).sbom.spdx.json
SBOM_STAMP = $(STAMPDIR)/sbom
SBOM_TOOLS_DIR = $(BUILDDIR)/sbom-tools/bin
SBOM_INSTALL_TOOLS = $(SCRIPTDIR)/install-sbom-tools.sh

PHONY += sbom sbom-clean sbom-vuln-scan

sbom: $(SBOM_STAMP)
$(SBOM_STAMP): $(SYSROOT_COMPLETE_STAMP)
$(Q) rm -f $@ && eval $(PROFILE_STAMP)
$(Q) echo "==== Generating SBOM for $(MACHINE) ===="
$(Q) export PATH="$(SBOM_TOOLS_DIR):$$PATH" ; \
command -v askalono >/dev/null 2>&1 && command -v cyclonedx-cli >/dev/null 2>&1 || { \
echo "==== Provisioning SBOM tools into $(SBOM_TOOLS_DIR) ====" ; \
"$(SBOM_INSTALL_TOOLS)" "$(SBOM_TOOLS_DIR)" || \
echo " WARNING: SBOM tool provisioning failed; SBOM will be degraded (NOASSERTION licenses, CycloneDX only)" ; } ; \
python3 $(SCRIPTDIR)/gen-sbom.py \
--machine $(MACHINE) \
--output $(SBOM_CDX) \
--spdx-output $(SBOM_SPDX)
$(Q) touch $@

# Optional: scan the already-generated SBOM for known vulnerabilities (grype),
# with CVEs already fixed by ONIE patches suppressed via OpenVEX.
#
# This is pure post-processing of the SBOM artifact, so it deliberately does NOT
# depend on $(SBOM_STAMP) (and therefore not on $(SYSROOT_COMPLETE_STAMP)).
# Depending on the stamp made a standalone `make sbom-vuln-scan` re-assemble the
# whole rootfs: the sysroot stamp is invalidated on every parse by the rootconf
# -cnewer guard in images.make, so a second invocation (e.g. the CI scan step,
# run after `make all` already produced the SBOM) rebuilt busybox/e2fsprogs/...
# before scanning. Decoupled, the scan just consumes $(SBOM_CDX); if it is
# missing we fail with a clear message instead of silently rebuilding the image.
sbom-vuln-scan:
$(Q) test -f $(SBOM_CDX) || { \
echo "ERROR: SBOM not found: $(SBOM_CDX)" >&2 ; \
echo " Generate it first: make MACHINE=$(MACHINE) sbom (or 'make all')." >&2 ; \
exit 1 ; }
$(Q) export PATH="$(SBOM_TOOLS_DIR):$$PATH" ; \
command -v grype >/dev/null 2>&1 || "$(SBOM_INSTALL_TOOLS)" "$(SBOM_TOOLS_DIR)" || true ; \
python3 $(SCRIPTDIR)/sbom-vuln-scan.py \
--machine $(MACHINE) \
--sbom $(SBOM_CDX) \
--output $(IMAGEDIR)/$(MACHINE_PREFIX).sbom.vulns.json

sbom-clean:
$(Q) rm -f $(SBOM_STAMP) $(SBOM_CDX) $(SBOM_SPDX) \
$(IMAGEDIR)/$(MACHINE_PREFIX).sbom.vulns.json \
$(IMAGEDIR)/$(MACHINE_PREFIX).sbom.vulns.vex.json \
$(IMAGEDIR)/$(MACHINE_PREFIX).sbom.vulns.md
$(Q) echo "=== Finished making $@ for $(PLATFORM)"

#-------------------------------------------------------------------------------
#
# Local Variables:
# mode: makefile-gmake
# End:
Loading