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
166 changes: 166 additions & 0 deletions .github/workflows/sbom-canary.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
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/.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Worth a line here that both products build from their default branch, so a break there fails our share/ PRs.


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
Comment thread
sameehj marked this conversation as resolved.
Resolved

- name: Copy share into the product
run: |
python3 tooling/tools/wolfssl-compliance-tooling-sync \
--src tooling \
--dest product

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please pass --prune. Without it, a file removed from share/ stays in the product, which hides the break this job should catch.


- 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: 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
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
5 changes: 5 additions & 0 deletions share/build/sbom.mk
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CMake helper needs the same alias. wolfBoot cmake/sbom.cmake:116 and wolfTPM CMakeLists.txt:1156 call wolfglass_add_sbom(), which build/sbom.cmake no longer defines. With wolfTPM's arguments against this branch, configure fails with Unknown CMake command "wolfglass_add_sbom". This fixes it:

macro(wolfglass_add_sbom)
    wolfssl_compliance_tooling_add_sbom(${ARGN})
endmacro()

The canary misses it because neither job builds through CMake. A CMake step in the wolfTPM job would cover it.

Line 197 restates line 196 and can go.


SBOM_TARGET ?= sbom
$(eval $(call wolfssl_compliance_tooling_sbom_rule,$(SBOM_TARGET),SBOM_))
2 changes: 1 addition & 1 deletion share/frontends/compdb_sbom.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
27 changes: 18 additions & 9 deletions tests/test_low_findings.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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):
Expand All @@ -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:
Expand Down
Loading