sbom: re-vendor wolfGlass tip and harden Make-path SBOM tooling

Sync tools/sbom to wolfGlass 9bdf5b7: document --cflags -D-only
behavior, add WOLFSSL_DIR/version.h --dep-version fallback with correct
$$$$ expansion, and teach validate_sbom.py --min-properties. CI Make
path checks now require a non-empty property set so empty captures
cannot pass.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
pull/824/head
Sameeh Jubran 2026-07-27 17:13:09 +03:00 committed by Daniele Lacamera
parent 66f6701a02
commit c0c8cf2e4f
6 changed files with 105 additions and 22 deletions

View File

@ -83,7 +83,7 @@ jobs:
cp config/examples/sim.config .config
make sbom TARGET=sim
python3 tools/sbom/validate_sbom.py \
--name-prefix wolfboot \
--name-prefix wolfboot --min-properties 20 \
wolfboot-*.cdx.json wolfboot-*.spdx.json
- name: CMake path - cmake --target sbom (sim)
@ -236,7 +236,7 @@ jobs:
echo "ERROR: PICO_SDK_PATH macro was dropped (should be redacted)." >&2
exit 1; }
python3 tools/sbom/validate_sbom.py \
--name-prefix wolfboot \
--name-prefix wolfboot --min-properties 20 \
wolfboot-*.cdx.json wolfboot-*.spdx.json
echo "rp2350 scrub OK: path redacted, macro key preserved"
@ -303,7 +303,7 @@ jobs:
cp config/examples/${{ matrix.target }}.config .config
make sbom TARGET=${{ matrix.target }}
python3 tools/sbom/validate_sbom.py \
--name-prefix wolfboot \
--name-prefix wolfboot --min-properties 20 \
wolfboot-*.cdx.json wolfboot-*.spdx.json
- name: Upload SBOM artifact

View File

@ -1 +1 @@
1f1f7f96254d419e4d41d1b8d8991903456bec22
9bdf5b7133c603659ea34b9a552c0bf2a962a8c0

View File

@ -1,10 +1,8 @@
# wolfBoot SBOM Toolkit
# share/
This directory is the vendored wolfGlass SBOM layer for wolfBoot. It was synced
into `tools/sbom/` and pinned with `VERSION` and `.wolfglass-rev`.
For future updates, refresh this directory from wolfGlass with
`tools/wolfglass-sync` rather than editing the shared files ad hoc.
This is the only vendorable set. Use `tools/wolfglass-sync` to copy these files
into a product at `tools/sbom/`, together with the pin files (`VERSION` and
`.wolfglass-rev`). Do not copy the `share/` folder name; copy the files.
## Contents
@ -49,7 +47,7 @@ The driver captures macros with the host compiler, so the SBOM is reproducible
across toolchains. It scrubs absolute host paths from the captured macros unless
you pass `--no-scrub`.
The shared driver is product-neutral and calls the vendored `tools/sbom/gen-sbom` by
The shared driver is product-neutral and calls the vendored `share/gen-sbom` by
default. Pass `--gen-sbom` only when you want to override that copy.
## The manifest contract

View File

@ -15,7 +15,13 @@
#
# Config - set one:
# SBOM_CFLAGS Build CFLAGS whose -D tokens describe the config.
# SBOM_OPTIONS_H A pre-expanded flat #define header.
# NOTE: the driver keeps ONLY -D tokens from SBOM_CFLAGS.
# -I / -include / other flags are dropped. Products whose
# config comes from -include'ing a settings header (e.g.
# wolfHSM's wh_settings.h) MUST capture with
# `$(HOSTCC) -dM -E ... -include ...` themselves and pass
# the dump via SBOM_OPTIONS_H, not SBOM_CFLAGS.
# SBOM_OPTIONS_H A pre-expanded flat #define header (verbatim, scrubbed).
# SBOM_USER_SETTINGS A user_settings.h.
# SBOM_SOURCE_ONLY = 1 Source-inventory SBOM with no build-config macros.
#
@ -33,12 +39,29 @@
# SBOM_NO_ARTIFACT_HASH = 1 As-built FIPS/kernel: do not re-hash.
# SBOM_DEP_WOLFSSL yes/no - record wolfSSL as a dependency.
# SBOM_DEP_OPENSSL yes/no - record OpenSSL as a dependency.
# SBOM_WOLFSSL_VERSION Explicit wolfSSL version for --dep-version. When
# unset and SBOM_DEP_WOLFSSL=yes, falls back to
# $(WOLFSSL_DIR)/wolfssl/version.h
# (LIBWOLFSSL_VERSION_STRING), matching sbom.am.
# SBOM_DEP_VERSION Extra --dep-version KEY=VER tokens (space-separated).
# HOSTCC Host C compiler for macro capture. Default: cc.
# CRA_PYTHON Python interpreter. Default: python3.
#
# The driver path is derived from this fragment's own location, so a product
# that vendors share/ into tools/sbom/ needs no path configuration.
#
# Source-list staging uses $(CURDIR)/.<target>-wolfglass-srcs.txt (not mktemp).
# GNU Make expands $${TMPDIR:-/tmp} as an empty Make variable named
# "TMPDIR:-/tmp", which produced "/wolfglass-srcs.XXXXXX" and broke every
# host. The CURDIR file is .gitignore'd; avoid parallel make -j of the *same*
# SBOM target (two recipes would share one staging file). Distinct targets
# (sbom vs sbom-hal) use distinct filenames via $(1).
#
# Shell variables inside wolfglass_sbom_rule need $$$$name (not $$name):
# $(call)/$(eval) expands the define once, then the recipe expands again.
# $$name becomes $n + ame (empty single-letter Make var) after that double
# expansion; $$$$name survives as $name for the shell.
#
# To instantiate a second target, set another variable prefix and call:
# $(eval $(call wolfglass_sbom_rule,sbom-hal,SBOM_HAL_))
# using SBOM_HAL_NAME, SBOM_HAL_SRCS, SBOM_HAL_CFLAGS, and so on.
@ -61,6 +84,23 @@ $(1):
trap 'rm -f "$(CURDIR)/.$(1)-wolfglass-srcs.txt"' EXIT INT TERM HUP; \
printf '%s\n' $($(2)SRCS) > "$(CURDIR)/.$(1)-wolfglass-srcs.txt"; \
fi; \
dep_ver=""; \
if [ -n "$($(2)DEP_VERSION)" ]; then \
for dv in $($(2)DEP_VERSION); do \
dep_ver="$$$$dep_ver --dep-version $$$$dv"; \
done; \
fi; \
if [ "$($(2)DEP_WOLFSSL)" = "yes" ] || [ "$($(2)DEP_WOLFSSL)" = "1" ]; then \
wv="$($(2)WOLFSSL_VERSION)"; \
if [ -z "$$$$wv" ] && [ -n "$(WOLFSSL_DIR)" ] && \
[ -f "$(WOLFSSL_DIR)/wolfssl/version.h" ]; then \
wv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
"$(WOLFSSL_DIR)/wolfssl/version.h" | head -1`; \
fi; \
if [ -n "$$$$wv" ]; then \
dep_ver="$$$$dep_ver --dep-version wolfssl=$$$$wv"; \
fi; \
fi; \
CRA_PYTHON="$(CRA_PYTHON)" HOSTCC="$(or $($(2)HOSTCC),$(HOSTCC))" \
"$(or $($(2)DRIVER),$(SBOM_DRIVER))" \
--name "$($(2)NAME)" \
@ -79,6 +119,7 @@ $(1):
$(if $($(2)VERSION_MACRO),--version-macro "$($(2)VERSION_MACRO)") \
$(if $($(2)DEP_WOLFSSL),--dep-wolfssl "$($(2)DEP_WOLFSSL)") \
$(if $($(2)DEP_OPENSSL),--dep-openssl "$($(2)DEP_OPENSSL)") \
$$$$dep_ver \
$(if $(or $($(2)GEN),$(GEN_SBOM)),--gen-sbom "$(or $($(2)GEN),$(GEN_SBOM))") \
$(if $($(2)CDX_OUT),--cdx-out "$($(2)CDX_OUT)") \
$(if $($(2)SPDX_OUT),--spdx-out "$($(2)SPDX_OUT)")

View File

@ -120,7 +120,13 @@ def gen_sbom_supports(python, gen_sbom, flag):
def capture_macros(hostcc, cflags):
"""Expand the -D tokens of CFLAGS through the host compiler's -dM -E."""
"""Expand the -D tokens of CFLAGS through the host compiler's -dM -E.
Only tokens that start with ``-D`` are kept. ``-I``, ``-include``, and
every other flag are dropped. Products whose configuration is expressed
by ``-include``'ing a settings header must capture that header themselves
(``hostcc -dM -E ... -include ...``) and pass the dump via ``--options-h``.
"""
defs = [t for t in cflags.split() if t.startswith("-D")]
cmd = [hostcc, "-dM", "-E", "-DWOLFSSL_USER_SETTINGS", *defs,
"-x", "c", os.devnull]

View File

@ -11,15 +11,21 @@ on a broken generator. It is not a full schema validator.
* metadata.component.name starts with --name-prefix (if given)
* metadata.component has a non-empty version
* at least one component or component property recorded
* optional --min-properties N on metadata.component.properties
* optional --require-dep-version NAME: a components[] entry with that
name must exist and carry a non-empty version
SPDX (*.spdx.json):
* spdxVersion starts with "SPDX-2"
* has a name and at least one package
* optional --require-dep-version NAME: a packages[] entry whose name
contains NAME must carry a non-empty versionInfo
The file kind is detected by content, so argument order does not matter.
Usage:
validate_sbom.py [--name-prefix PREFIX] FILE [FILE ...]
validate_sbom.py [--name-prefix PREFIX] [--min-properties N]
[--require-dep-version NAME] FILE [FILE ...]
"""
import argparse
@ -32,7 +38,7 @@ def fail(path, msg):
sys.exit(1)
def validate_cyclonedx(path, d, name_prefix):
def validate_cyclonedx(path, d, name_prefix, min_properties, require_deps):
if d.get("bomFormat") != "CycloneDX":
fail(path, f"bomFormat != CycloneDX (got {d.get('bomFormat')!r})")
if d.get("specVersion") != "1.6":
@ -44,21 +50,45 @@ def validate_cyclonedx(path, d, name_prefix):
f"{name_prefix!r} (got {name!r})")
if not comp.get("version"):
fail(path, "metadata.component.version is empty")
if not d.get("components") and not comp.get("properties"):
props = comp.get("properties") or []
if not d.get("components") and not props:
fail(path, "no components or component properties recorded")
if min_properties is not None and len(props) < min_properties:
fail(path, f"metadata.component.properties has {len(props)} entries, "
f"need at least {min_properties} (config capture likely "
f"empty — check --options-h vs --cflags)")
for dep_name in require_deps:
matches = [c for c in (d.get("components") or [])
if c.get("name") == dep_name]
if not matches:
fail(path, f"required dependency component {dep_name!r} missing")
if not matches[0].get("version"):
fail(path, f"dependency component {dep_name!r} has no version "
f"(pass --dep-version or set WOLFSSL_DIR)")
print(f"OK [{path}]: CycloneDX 1.6, component "
f"{comp.get('name')} {comp.get('version')}")
f"{comp.get('name')} {comp.get('version')}, "
f"{len(props)} properties")
def validate_spdx(path, d):
def validate_spdx(path, d, require_deps):
ver = d.get("spdxVersion", "")
if not ver.startswith("SPDX-2"):
fail(path, f"spdxVersion not SPDX-2.x (got {ver!r})")
if not d.get("name"):
fail(path, "document name is empty")
if not d.get("packages"):
pkgs = d.get("packages") or []
if not pkgs:
fail(path, "no packages recorded")
print(f"OK [{path}]: {ver}, {len(d['packages'])} package(s)")
for dep_name in require_deps:
matches = [p for p in pkgs
if dep_name.lower() in (p.get("name") or "").lower()]
if not matches:
fail(path, f"required dependency package matching {dep_name!r} "
f"missing")
if not matches[0].get("versionInfo"):
fail(path, f"dependency package {matches[0].get('name')!r} has "
f"no versionInfo")
print(f"OK [{path}]: {ver}, {len(pkgs)} package(s)")
def main(argv):
@ -67,6 +97,13 @@ def main(argv):
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--name-prefix", default="",
help="Require metadata.component.name to start with this.")
ap.add_argument("--min-properties", type=int, default=None,
help="Require at least N CycloneDX component properties "
"(guards empty --cflags captures).")
ap.add_argument("--require-dep-version", action="append", default=[],
metavar="NAME",
help="Require a dependency component/package NAME with "
"a non-empty version (repeatable).")
ap.add_argument("files", nargs="+")
args = ap.parse_args(argv[1:])
@ -79,9 +116,10 @@ def main(argv):
except json.JSONDecodeError as e:
fail(path, f"invalid JSON: {e}")
if "bomFormat" in d or path.endswith(".cdx.json"):
validate_cyclonedx(path, d, args.name_prefix)
validate_cyclonedx(path, d, args.name_prefix,
args.min_properties, args.require_dep_version)
elif "spdxVersion" in d or path.endswith(".spdx.json"):
validate_spdx(path, d)
validate_spdx(path, d, args.require_dep_version)
else:
fail(path, "unrecognized SBOM format (neither CycloneDX nor SPDX)")
print("All SBOMs valid.")