From f13a8cf098dbffec41fcd12e4658a758d44a6ed2 Mon Sep 17 00:00:00 2001 From: Asher Feldman Date: Thu, 18 Jun 2026 12:31:09 -0700 Subject: [PATCH 1/2] feat(build): attested sbom --- .github/workflows/matrix.yml | 24 +++++ hack/build/generate-sbom.py | 170 +++++++++++++++++++++++++++++++++++ hack/build/matrix.py | 4 + 3 files changed, 198 insertions(+) create mode 100644 hack/build/generate-sbom.py diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index 12933fb..461c658 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -150,6 +150,8 @@ jobs: KERNEL_VERSION: "${{ matrix.merges.version }}" KERNEL_FLAVOR: "${{ matrix.merges.flavor }}" KERNEL_PRODUCES: "${{ join(matrix.merges.produces, ',') }}" + KERNEL_SRC_URL: "${{ matrix.merges.source }}" + FIRMWARE_URL: "${{ matrix.merges.firmware_url }}" DIGESTS_DIR: digests steps: - name: Harden the runner (Audit all outbound calls) @@ -188,3 +190,25 @@ jobs: compression-level: 0 - name: run merge script run: sh -x merge.sh + - name: install python deps for SBOM + run: pip3 install -r requirements.txt + - name: generate and attest CycloneDX SBOM + env: + COSIGN_EXPERIMENTAL: "true" + run: | + set -euo pipefail + python3 hack/build/generate-sbom.py + jq -e '.bomFormat == "CycloneDX" and ((.components // []) | length > 0)' \ + sbom.cdx.json >/dev/null \ + || { echo "::error::kernel SBOM is empty or invalid"; exit 1; } + # Attest the SBOM to each published manifest (kernel + SDK) + attested="" + IFS=',' read -ra refs <<< "${KERNEL_PRODUCES}" + for ref in "${refs[@]}"; do + [ -n "${ref}" ] || continue + image="${ref%:*}" + case " ${attested} " in *" ${image} "*) continue ;; esac + attested="${attested} ${image}" + echo "attesting SBOM to ${ref}" + cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "${ref}" + done diff --git a/hack/build/generate-sbom.py b/hack/build/generate-sbom.py new file mode 100644 index 0000000..4fbbc8e --- /dev/null +++ b/hack/build/generate-sbom.py @@ -0,0 +1,170 @@ +#!/usr/bin/env python3 +"""Generate a curated CycloneDX SBOM for a published kernel manifest. + +The kernel / kernel-SDK images are `FROM scratch` containing only compiled +artifacts, so scanning them yields nothing, and scanning the debian build +container yields hundreds of toolchain/base-OS packages that have no bearing on +the kernel that ships. Instead we describe what actually defines the kernel: + + - the upstream linux source version it was built from, + - the patches applied to that source (the authoritative, arch-union list from + patchlist.py), and + - for GPU flavors, the firmware / nvidia module versions baked in. + +All of this is architecture-independent -- the same source, patches, and module +versions apply to every arch in the manifest -- so a single SBOM correctly +describes the whole multi-arch image and stays correct as new arches (e.g. +arm64) are added. There are deliberately NO per-arch / package filter rules. + +Reads from the environment (set by the merge job): + KERNEL_VERSION e.g. "6.18.35" or "6.18.35+nvidia-610.43.02" + KERNEL_FLAVOR e.g. "zone", "host", "zone-amdgpu", "zone-nvidiagpu" + KERNEL_SRC_URL upstream linux source tarball URL + FIRMWARE_URL linux-firmware tarball URL (only used for zone-amdgpu) + +Writes sbom.cdx.json (CycloneDX 1.6) in the current directory. + +This was created with Claude. +""" +import json +import os +import re +import subprocess +import sys + + +def applied_patches(version, flavor): + """The patch files applied to this (version, flavor). + + Delegates to patchlist.py so the SBOM lists exactly the patches the build + applies. patchlist.py matches constraints WITHOUT an arch argument, so this + is the union across architectures -- correct for a manifest-level SBOM and + forward-compatible with future arch-specific patches. + """ + out = subprocess.run( + [sys.executable, "hack/build/patchlist.py", version, flavor], + check=True, + stdout=subprocess.PIPE, + text=True, + ).stdout + return [line.strip() for line in out.splitlines() if line.strip()] + + +def firmware_version_from_url(url): + # .../linux-firmware-.tar.xz + match = re.search(r"linux-firmware-(.+?)\.tar\.", url or "") + return match.group(1) if match else None + + +def main(): + version = os.environ["KERNEL_VERSION"] + flavor = os.environ["KERNEL_FLAVOR"] + src_url = os.environ.get("KERNEL_SRC_URL", "") + firmware_url = os.environ.get("FIRMWARE_URL", "") + + # Strip any "+nvidia-" local suffix to get the upstream kernel version. + kernel_version = version.split("+")[0] + + kernel_ref = "pkg:generic/linux@%s" % kernel_version + linux_component = { + "bom-ref": kernel_ref, + "type": "operating-system", + "name": "linux", + "version": kernel_version, + "purl": kernel_ref, + } + if src_url: + linux_component["externalReferences"] = [ + {"type": "distribution", "url": src_url} + ] + + patches = applied_patches(version, flavor) + if patches: + linux_component["pedigree"] = { + "patches": [ + {"type": "unofficial", "diff": {"url": patch}} for patch in patches + ] + } + + components = [linux_component] + depends_on = [kernel_ref] + + # GPU flavors bake in extra, separately-versioned artifacts. + if flavor == "zone-amdgpu": + fw_version = firmware_version_from_url(firmware_url) + if fw_version: + fw_ref = "pkg:generic/linux-firmware@%s" % fw_version + fw_component = { + "bom-ref": fw_ref, + "type": "firmware", + "name": "linux-firmware", + "version": fw_version, + "purl": fw_ref, + } + if firmware_url: + fw_component["externalReferences"] = [ + {"type": "distribution", "url": firmware_url} + ] + components.append(fw_component) + depends_on.append(fw_ref) + + if flavor == "zone-nvidiagpu" and "+nvidia-" in version: + nv_version = version.split("+nvidia-", 1)[1] + nv_url = ( + "https://github.com/NVIDIA/open-gpu-kernel-modules/archive/refs/tags/%s.tar.gz" + % nv_version + ) + nv_ref = "pkg:github/NVIDIA/open-gpu-kernel-modules@%s" % nv_version + components.append( + { + "bom-ref": nv_ref, + "type": "library", + "name": "nvidia-open-gpu-kernel-modules", + "version": nv_version, + "purl": nv_ref, + "externalReferences": [{"type": "distribution", "url": nv_url}], + } + ) + depends_on.append(nv_ref) + + image_ref = "%s-kernel@%s" % (flavor, version) + document = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": { + "bom-ref": image_ref, + "type": "container", + "name": "%s-kernel" % flavor, + "version": version, + }, + "properties": [ + {"name": "dev.edera.kernel.flavor", "value": flavor}, + ], + }, + "components": components, + "dependencies": [{"ref": image_ref, "dependsOn": depends_on}], + } + + with open("sbom.cdx.json", "w") as out: + json.dump(document, out, indent=2) + out.write("\n") + + # Human-readable summary for the build log. + print( + json.dumps( + { + "flavor": flavor, + "version": version, + "kernel": kernel_version, + "patches": len(patches), + "components": len(components), + }, + indent=2, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/hack/build/matrix.py b/hack/build/matrix.py index e0e18bd..9909aae 100644 --- a/hack/build/matrix.py +++ b/hack/build/matrix.py @@ -487,6 +487,10 @@ def generate_merges(builds: list[dict[str, any]]) -> list[dict[str, any]]: "tags": list(build["tags"]), "produces": list(build["produces"]), "archs": [build["arch"]], + # Carried for SBOM generation in the merge job; identical across + # archs for a given (version, flavor). + "source": build["source"], + "firmware_url": build["firmware_url"], } else: if build["arch"] not in merges[key]["archs"]: From 1068809d321cb1902bfc3d6f2809db4cfd942ca5 Mon Sep 17 00:00:00 2001 From: Asher Feldman Date: Tue, 23 Jun 2026 10:23:09 -0700 Subject: [PATCH 2/2] feat(build): pinned python for sbom build --- .github/workflows/matrix.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/matrix.yml b/.github/workflows/matrix.yml index 461c658..e50c5e7 100644 --- a/.github/workflows/matrix.yml +++ b/.github/workflows/matrix.yml @@ -160,6 +160,12 @@ jobs: egress-policy: audit - name: checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v4 + - name: set up Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.13' + - name: install python deps + run: pip3 install -r requirements.txt - name: install cosign uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 - name: docker setup buildx @@ -190,8 +196,6 @@ jobs: compression-level: 0 - name: run merge script run: sh -x merge.sh - - name: install python deps for SBOM - run: pip3 install -r requirements.txt - name: generate and attest CycloneDX SBOM env: COSIGN_EXPERIMENTAL: "true"