From d7e0d5c3d20e47896a4ac59d0ac53b3e4079c2e7 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Tue, 20 Oct 2026 17:35:05 +0300 Subject: [PATCH 1/4] ci: prove a share change still generates product SBOMs Overlay share/ onto wolfBoot and wolfTPM and generate each SBOM. Keep the old Make macro name so the current wolfBoot Makefile still parses. --- .github/workflows/sbom-canary.yml | 159 ++++++++++++++++++++++++++++++ share/build/sbom.mk | 5 + 2 files changed, 164 insertions(+) create mode 100644 .github/workflows/sbom-canary.yml diff --git a/.github/workflows/sbom-canary.yml b/.github/workflows/sbom-canary.yml new file mode 100644 index 0000000..f6b8bb6 --- /dev/null +++ b/.github/workflows/sbom-canary.yml @@ -0,0 +1,159 @@ +name: SBOM product canary + +# On a change to the vendored set, copy share/ into a temporary checkout of +# each ready product and generate that product's SBOM. The product +# repositories keep their pinned copy. This job does not push to them. +# +# wolfBoot is the plain-Make product. wolfTPM is the autotools product. +# Add a product here only after it vendors tools/sbom/. + +on: + pull_request: + paths: + - 'share/**' + - 'VERSION' + - 'tools/wolfssl-compliance-tooling-sync' + - '.github/workflows/sbom-canary.yml' + push: + branches: [master] + paths: + - 'share/**' + - 'VERSION' + - 'tools/wolfssl-compliance-tooling-sync' + - '.github/workflows/sbom-canary.yml' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: sbom-canary-${{ github.ref }} + cancel-in-progress: true + +jobs: + canary: + name: ${{ matrix.product }} + runs-on: ubuntu-latest + timeout-minutes: 40 + strategy: + fail-fast: false + matrix: + include: + - product: wolfboot + repository: wolfSSL/wolfBoot + name-prefix: wolfboot + - product: wolftpm + repository: wolfSSL/wolfTPM + name-prefix: wolftpm + steps: + - name: Checkout wolfssl-compliance-tooling + uses: actions/checkout@v4 + with: + path: tooling + + - name: Checkout ${{ matrix.product }} + uses: actions/checkout@v4 + with: + repository: ${{ matrix.repository }} + path: product + + - name: Checkout wolfSSL + if: matrix.product == 'wolftpm' + uses: actions/checkout@v4 + with: + repository: wolfSSL/wolfssl + path: wolfssl + + - name: Install build tools + run: | + sudo apt-get update + sudo apt-get install -y build-essential autoconf automake libtool \ + pkg-config + + - name: Copy share into the product + run: | + python3 tooling/tools/wolfssl-compliance-tooling-sync \ + --src tooling \ + --dest product + + - name: Fetch the wolfBoot wolfSSL submodule + if: matrix.product == 'wolfboot' + working-directory: product + run: git submodule update --init --depth 1 -- lib/wolfssl + + - name: Generate the wolfBoot SBOM + if: matrix.product == 'wolfboot' + working-directory: product + run: | + cp config/examples/sim.config .config + make sbom + + - name: Build and install wolfSSL + if: matrix.product == 'wolftpm' + working-directory: wolfssl + run: | + autoreconf -ivf + ./configure --enable-wolftpm --enable-pkcallbacks \ + --prefix="$GITHUB_WORKSPACE/wolfssl-install" + make -j"$(nproc)" + make install + + - name: Configure and build wolfTPM + if: matrix.product == 'wolftpm' + working-directory: product + run: | + autoreconf -ivf + ./configure --enable-swtpm --disable-fwtpm --disable-examples \ + --with-wolfcrypt="$GITHUB_WORKSPACE/wolfssl-install" + make -j"$(nproc)" + + - name: Generate the wolfTPM SBOM + if: matrix.product == 'wolftpm' + working-directory: product + run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + + - name: Validate the SBOM + working-directory: product + env: + NAME_PREFIX: ${{ matrix.name-prefix }} + run: | + shopt -s nullglob + files=("${NAME_PREFIX}"-*.cdx.json "${NAME_PREFIX}"-*.spdx.json) + if [ "${#files[@]}" -eq 0 ]; then + echo "ERROR: no SBOM files matched ${NAME_PREFIX}-*.cdx.json or ${NAME_PREFIX}-*.spdx.json." >&2 + exit 1 + fi + python3 tools/sbom/validate_sbom.py \ + --name-prefix "$NAME_PREFIX" \ + "${files[@]}" + + - name: Assert no host path leak + working-directory: product + env: + NAME_PREFIX: ${{ matrix.name-prefix }} + run: | + shopt -s nullglob + files=("${NAME_PREFIX}"-*.cdx.json "${NAME_PREFIX}"-*.spdx.json) + # A published SBOM must not contain build-machine absolute paths. + # grep exit 0 = match (fail), 1 = clean, >=2 = real error. + set +e + out="$(grep -REn '"/(home|Users|root)/' "${files[@]}")" + rc=$? + if [ "$rc" -eq 0 ]; then + echo "ERROR: absolute host path found in SBOM:" >&2 + echo "$out" >&2 + exit 1 + elif [ "$rc" -ne 1 ]; then + echo "ERROR: could not scan SBOM outputs." >&2 + exit 1 + fi + echo "OK: no host path leak." + + - name: Upload SBOM + uses: actions/upload-artifact@v4 + with: + name: sbom-canary-${{ matrix.product }} + path: | + product/${{ matrix.name-prefix }}-*.cdx.json + product/${{ matrix.name-prefix }}-*.spdx.json + if-no-files-found: error diff --git a/share/build/sbom.mk b/share/build/sbom.mk index c4554c3..9091e3b 100644 --- a/share/build/sbom.mk +++ b/share/build/sbom.mk @@ -193,5 +193,10 @@ $(1): $($(2)PREREQS) $(if $($(2)SPDX_OUT),--spdx-out "$($(2)SPDX_OUT)") endef +# Products vendored before the rename still call wolfglass_sbom_rule. +# Keep that name so a sync does not break their Makefile. +# Use a recursive reference, not $(value): $(call) must expand $(1) and $(2). +wolfglass_sbom_rule = $(wolfssl_compliance_tooling_sbom_rule) + SBOM_TARGET ?= sbom $(eval $(call wolfssl_compliance_tooling_sbom_rule,$(SBOM_TARGET),SBOM_)) From 92da75448be5138c0ddd76c5aeb1961b0cc5e991 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Tue, 20 Oct 2026 17:42:00 +0300 Subject: [PATCH 2/4] ci: install pyspdxtools before the wolfTPM SBOM The autotools fragment stops when pyspdxtools is absent. --- .github/workflows/sbom-canary.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/sbom-canary.yml b/.github/workflows/sbom-canary.yml index f6b8bb6..d11e1d5 100644 --- a/.github/workflows/sbom-canary.yml +++ b/.github/workflows/sbom-canary.yml @@ -110,7 +110,11 @@ jobs: - name: Generate the wolfTPM SBOM if: matrix.product == 'wolftpm' working-directory: product - run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + run: | + # sbom.am checks for pyspdxtools before it runs gen-sbom. + python3 -m pip install --user 'spdx-tools==0.8.*' + export PATH="$HOME/.local/bin:$PATH" + make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" - name: Validate the SBOM working-directory: product From 8ef25b97f3b55092c74b70b4081dfa70c2e3d79e Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Tue, 20 Oct 2026 17:48:00 +0300 Subject: [PATCH 3/4] ci: install pyspdxtools before wolfTPM configure configure records PYSPDXTOOLS. A later install leaves that variable empty. --- .github/workflows/sbom-canary.yml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/.github/workflows/sbom-canary.yml b/.github/workflows/sbom-canary.yml index d11e1d5..bcba03a 100644 --- a/.github/workflows/sbom-canary.yml +++ b/.github/workflows/sbom-canary.yml @@ -98,6 +98,13 @@ jobs: make -j"$(nproc)" make install + - name: Install pyspdxtools + if: matrix.product == 'wolftpm' + run: | + # configure records PYSPDXTOOLS. Install the tool before configure. + python3 -m pip install --user 'spdx-tools==0.8.*' + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + - name: Configure and build wolfTPM if: matrix.product == 'wolftpm' working-directory: product @@ -110,11 +117,7 @@ jobs: - name: Generate the wolfTPM SBOM if: matrix.product == 'wolftpm' working-directory: product - run: | - # sbom.am checks for pyspdxtools before it runs gen-sbom. - python3 -m pip install --user 'spdx-tools==0.8.*' - export PATH="$HOME/.local/bin:$PATH" - make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" + run: make sbom WOLFSSL_DIR="$GITHUB_WORKSPACE/wolfssl" - name: Validate the SBOM working-directory: product From da30e727779126ff4f45d5c1f3cb27cfc1ca2251 Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Thu, 22 Oct 2026 14:06:43 +0300 Subject: [PATCH 4/4] fix(tests): load extensionless modules on Python 3.15 SourceFileLoader.load_module is gone in Python 3.15, so the low-findings self-test exited before it ran. Use exec_module, and pass maxsplit to re.split by name. This is the same fix that landed with #36. --- share/frontends/compdb_sbom.py | 2 +- tests/test_low_findings.py | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/share/frontends/compdb_sbom.py b/share/frontends/compdb_sbom.py index 34f427a..804366c 100755 --- a/share/frontends/compdb_sbom.py +++ b/share/frontends/compdb_sbom.py @@ -51,7 +51,7 @@ def entry_tokens(entry): def macro_name(d): """Macro name of a -D/-U operand: the text before the first '=' or '(', so a function-like 'F(a)=a' is keyed as 'F'.""" - return re.split(r'[=(]', d, 1)[0] + return re.split(r'[=(]', d, maxsplit=1)[0] def extract_defines(tokens): diff --git a/tests/test_low_findings.py b/tests/test_low_findings.py index 2bb4b79..77c99f5 100644 --- a/tests/test_low_findings.py +++ b/tests/test_low_findings.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 """Regression tests for the low-severity review-findings bundle (gpex.20).""" +import importlib.util import json import os import stat @@ -15,12 +16,18 @@ GEN_SBOM = os.path.join(SHARE, "gen-sbom") VALIDATE = os.path.join(SHARE, "validate_sbom.py") -gs = SourceFileLoader("gen_sbom", GEN_SBOM).load_module() -drv = SourceFileLoader("sbom_driver", os.path.join(SHARE, "sbom-driver.py") - ).load_module() -compdb = SourceFileLoader( - "compdb_sbom", os.path.join(SHARE, "frontends", "compdb_sbom.py") -).load_module() +def _load(name, path): + loader = SourceFileLoader(name, path) + module = importlib.util.module_from_spec( + importlib.util.spec_from_loader(name, loader)) + loader.exec_module(module) + return module + + +gs = _load("gen_sbom", GEN_SBOM) +drv = _load("sbom_driver", os.path.join(SHARE, "sbom-driver.py")) +compdb = _load("compdb_sbom", + os.path.join(SHARE, "frontends", "compdb_sbom.py")) class TestAtomicWrite(unittest.TestCase): @@ -35,9 +42,11 @@ def test_writes_content_and_leaves_no_temp(self): def test_dev_stdout_redirected_to_file(self): # `--cdx-out /dev/stdout > out.json`: /dev/stdout is a symlink to the # redirected file and must be written through, not replaced. - code = ("from importlib.machinery import SourceFileLoader as L; " - "L('g', %r).load_module().write_json_atomic({'a': 1}, " - "'/dev/stdout')" % GEN_SBOM) + code = ("import importlib.util as u; " + "from importlib.machinery import SourceFileLoader as L; " + "l = L('g', %r); m = u.module_from_spec(" + "u.spec_from_loader('g', l)); l.exec_module(m); " + "m.write_json_atomic({'a': 1}, '/dev/stdout')" % GEN_SBOM) with tempfile.TemporaryDirectory() as d: out = os.path.join(d, "out.json") with open(out, "w") as f: