Skip to content

Commit e77f127

Browse files
committed
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>
1 parent b99d976 commit e77f127

6 files changed

Lines changed: 105 additions & 22 deletions

File tree

.github/workflows/test-sbom.yml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ jobs:
8383
cp config/examples/sim.config .config
8484
make sbom TARGET=sim
8585
python3 tools/sbom/validate_sbom.py \
86-
--name-prefix wolfboot \
86+
--name-prefix wolfboot --min-properties 20 \
8787
wolfboot-*.cdx.json wolfboot-*.spdx.json
8888
8989
- name: CMake path - cmake --target sbom (sim)
@@ -236,7 +236,7 @@ jobs:
236236
echo "ERROR: PICO_SDK_PATH macro was dropped (should be redacted)." >&2
237237
exit 1; }
238238
python3 tools/sbom/validate_sbom.py \
239-
--name-prefix wolfboot \
239+
--name-prefix wolfboot --min-properties 20 \
240240
wolfboot-*.cdx.json wolfboot-*.spdx.json
241241
echo "rp2350 scrub OK: path redacted, macro key preserved"
242242
@@ -303,7 +303,7 @@ jobs:
303303
cp config/examples/${{ matrix.target }}.config .config
304304
make sbom TARGET=${{ matrix.target }}
305305
python3 tools/sbom/validate_sbom.py \
306-
--name-prefix wolfboot \
306+
--name-prefix wolfboot --min-properties 20 \
307307
wolfboot-*.cdx.json wolfboot-*.spdx.json
308308
309309
- name: Upload SBOM artifact

tools/sbom/.wolfglass-rev

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1f1f7f96254d419e4d41d1b8d8991903456bec22
1+
9bdf5b7133c603659ea34b9a552c0bf2a962a8c0

tools/sbom/README.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
1-
# wolfBoot SBOM Toolkit
1+
# share/
22

3-
This directory is the vendored wolfGlass SBOM layer for wolfBoot. It was synced
4-
into `tools/sbom/` and pinned with `VERSION` and `.wolfglass-rev`.
5-
6-
For future updates, refresh this directory from wolfGlass with
7-
`tools/wolfglass-sync` rather than editing the shared files ad hoc.
3+
This is the only vendorable set. Use `tools/wolfglass-sync` to copy these files
4+
into a product at `tools/sbom/`, together with the pin files (`VERSION` and
5+
`.wolfglass-rev`). Do not copy the `share/` folder name; copy the files.
86

97
## Contents
108

@@ -49,7 +47,7 @@ The driver captures macros with the host compiler, so the SBOM is reproducible
4947
across toolchains. It scrubs absolute host paths from the captured macros unless
5048
you pass `--no-scrub`.
5149

52-
The shared driver is product-neutral and calls the vendored `tools/sbom/gen-sbom` by
50+
The shared driver is product-neutral and calls the vendored `share/gen-sbom` by
5351
default. Pass `--gen-sbom` only when you want to override that copy.
5452

5553
## The manifest contract

tools/sbom/build/sbom.mk

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
#
1616
# Config - set one:
1717
# SBOM_CFLAGS Build CFLAGS whose -D tokens describe the config.
18-
# SBOM_OPTIONS_H A pre-expanded flat #define header.
18+
# NOTE: the driver keeps ONLY -D tokens from SBOM_CFLAGS.
19+
# -I / -include / other flags are dropped. Products whose
20+
# config comes from -include'ing a settings header (e.g.
21+
# wolfHSM's wh_settings.h) MUST capture with
22+
# `$(HOSTCC) -dM -E ... -include ...` themselves and pass
23+
# the dump via SBOM_OPTIONS_H, not SBOM_CFLAGS.
24+
# SBOM_OPTIONS_H A pre-expanded flat #define header (verbatim, scrubbed).
1925
# SBOM_USER_SETTINGS A user_settings.h.
2026
# SBOM_SOURCE_ONLY = 1 Source-inventory SBOM with no build-config macros.
2127
#
@@ -33,12 +39,29 @@
3339
# SBOM_NO_ARTIFACT_HASH = 1 As-built FIPS/kernel: do not re-hash.
3440
# SBOM_DEP_WOLFSSL yes/no - record wolfSSL as a dependency.
3541
# SBOM_DEP_OPENSSL yes/no - record OpenSSL as a dependency.
42+
# SBOM_WOLFSSL_VERSION Explicit wolfSSL version for --dep-version. When
43+
# unset and SBOM_DEP_WOLFSSL=yes, falls back to
44+
# $(WOLFSSL_DIR)/wolfssl/version.h
45+
# (LIBWOLFSSL_VERSION_STRING), matching sbom.am.
46+
# SBOM_DEP_VERSION Extra --dep-version KEY=VER tokens (space-separated).
3647
# HOSTCC Host C compiler for macro capture. Default: cc.
3748
# CRA_PYTHON Python interpreter. Default: python3.
3849
#
3950
# The driver path is derived from this fragment's own location, so a product
4051
# that vendors share/ into tools/sbom/ needs no path configuration.
4152
#
53+
# Source-list staging uses $(CURDIR)/.<target>-wolfglass-srcs.txt (not mktemp).
54+
# GNU Make expands $${TMPDIR:-/tmp} as an empty Make variable named
55+
# "TMPDIR:-/tmp", which produced "/wolfglass-srcs.XXXXXX" and broke every
56+
# host. The CURDIR file is .gitignore'd; avoid parallel make -j of the *same*
57+
# SBOM target (two recipes would share one staging file). Distinct targets
58+
# (sbom vs sbom-hal) use distinct filenames via $(1).
59+
#
60+
# Shell variables inside wolfglass_sbom_rule need $$$$name (not $$name):
61+
# $(call)/$(eval) expands the define once, then the recipe expands again.
62+
# $$name becomes $n + ame (empty single-letter Make var) after that double
63+
# expansion; $$$$name survives as $name for the shell.
64+
#
4265
# To instantiate a second target, set another variable prefix and call:
4366
# $(eval $(call wolfglass_sbom_rule,sbom-hal,SBOM_HAL_))
4467
# using SBOM_HAL_NAME, SBOM_HAL_SRCS, SBOM_HAL_CFLAGS, and so on.
@@ -61,6 +84,23 @@ $(1):
6184
trap 'rm -f "$(CURDIR)/.$(1)-wolfglass-srcs.txt"' EXIT INT TERM HUP; \
6285
printf '%s\n' $($(2)SRCS) > "$(CURDIR)/.$(1)-wolfglass-srcs.txt"; \
6386
fi; \
87+
dep_ver=""; \
88+
if [ -n "$($(2)DEP_VERSION)" ]; then \
89+
for dv in $($(2)DEP_VERSION); do \
90+
dep_ver="$$$$dep_ver --dep-version $$$$dv"; \
91+
done; \
92+
fi; \
93+
if [ "$($(2)DEP_WOLFSSL)" = "yes" ] || [ "$($(2)DEP_WOLFSSL)" = "1" ]; then \
94+
wv="$($(2)WOLFSSL_VERSION)"; \
95+
if [ -z "$$$$wv" ] && [ -n "$(WOLFSSL_DIR)" ] && \
96+
[ -f "$(WOLFSSL_DIR)/wolfssl/version.h" ]; then \
97+
wv=`sed -n 's/.*LIBWOLFSSL_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
98+
"$(WOLFSSL_DIR)/wolfssl/version.h" | head -1`; \
99+
fi; \
100+
if [ -n "$$$$wv" ]; then \
101+
dep_ver="$$$$dep_ver --dep-version wolfssl=$$$$wv"; \
102+
fi; \
103+
fi; \
64104
CRA_PYTHON="$(CRA_PYTHON)" HOSTCC="$(or $($(2)HOSTCC),$(HOSTCC))" \
65105
"$(or $($(2)DRIVER),$(SBOM_DRIVER))" \
66106
--name "$($(2)NAME)" \
@@ -79,6 +119,7 @@ $(1):
79119
$(if $($(2)VERSION_MACRO),--version-macro "$($(2)VERSION_MACRO)") \
80120
$(if $($(2)DEP_WOLFSSL),--dep-wolfssl "$($(2)DEP_WOLFSSL)") \
81121
$(if $($(2)DEP_OPENSSL),--dep-openssl "$($(2)DEP_OPENSSL)") \
122+
$$$$dep_ver \
82123
$(if $(or $($(2)GEN),$(GEN_SBOM)),--gen-sbom "$(or $($(2)GEN),$(GEN_SBOM))") \
83124
$(if $($(2)CDX_OUT),--cdx-out "$($(2)CDX_OUT)") \
84125
$(if $($(2)SPDX_OUT),--spdx-out "$($(2)SPDX_OUT)")

tools/sbom/sbom-driver.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,13 @@ def gen_sbom_supports(python, gen_sbom, flag):
120120

121121

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

tools/sbom/validate_sbom.py

Lines changed: 47 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@
1111
* metadata.component.name starts with --name-prefix (if given)
1212
* metadata.component has a non-empty version
1313
* at least one component or component property recorded
14+
* optional --min-properties N on metadata.component.properties
15+
* optional --require-dep-version NAME: a components[] entry with that
16+
name must exist and carry a non-empty version
1417
1518
SPDX (*.spdx.json):
1619
* spdxVersion starts with "SPDX-2"
1720
* has a name and at least one package
21+
* optional --require-dep-version NAME: a packages[] entry whose name
22+
contains NAME must carry a non-empty versionInfo
1823
1924
The file kind is detected by content, so argument order does not matter.
2025
2126
Usage:
22-
validate_sbom.py [--name-prefix PREFIX] FILE [FILE ...]
27+
validate_sbom.py [--name-prefix PREFIX] [--min-properties N]
28+
[--require-dep-version NAME] FILE [FILE ...]
2329
"""
2430

2531
import argparse
@@ -32,7 +38,7 @@ def fail(path, msg):
3238
sys.exit(1)
3339

3440

35-
def validate_cyclonedx(path, d, name_prefix):
41+
def validate_cyclonedx(path, d, name_prefix, min_properties, require_deps):
3642
if d.get("bomFormat") != "CycloneDX":
3743
fail(path, f"bomFormat != CycloneDX (got {d.get('bomFormat')!r})")
3844
if d.get("specVersion") != "1.6":
@@ -44,21 +50,45 @@ def validate_cyclonedx(path, d, name_prefix):
4450
f"{name_prefix!r} (got {name!r})")
4551
if not comp.get("version"):
4652
fail(path, "metadata.component.version is empty")
47-
if not d.get("components") and not comp.get("properties"):
53+
props = comp.get("properties") or []
54+
if not d.get("components") and not props:
4855
fail(path, "no components or component properties recorded")
56+
if min_properties is not None and len(props) < min_properties:
57+
fail(path, f"metadata.component.properties has {len(props)} entries, "
58+
f"need at least {min_properties} (config capture likely "
59+
f"empty — check --options-h vs --cflags)")
60+
for dep_name in require_deps:
61+
matches = [c for c in (d.get("components") or [])
62+
if c.get("name") == dep_name]
63+
if not matches:
64+
fail(path, f"required dependency component {dep_name!r} missing")
65+
if not matches[0].get("version"):
66+
fail(path, f"dependency component {dep_name!r} has no version "
67+
f"(pass --dep-version or set WOLFSSL_DIR)")
4968
print(f"OK [{path}]: CycloneDX 1.6, component "
50-
f"{comp.get('name')} {comp.get('version')}")
69+
f"{comp.get('name')} {comp.get('version')}, "
70+
f"{len(props)} properties")
5171

5272

53-
def validate_spdx(path, d):
73+
def validate_spdx(path, d, require_deps):
5474
ver = d.get("spdxVersion", "")
5575
if not ver.startswith("SPDX-2"):
5676
fail(path, f"spdxVersion not SPDX-2.x (got {ver!r})")
5777
if not d.get("name"):
5878
fail(path, "document name is empty")
59-
if not d.get("packages"):
79+
pkgs = d.get("packages") or []
80+
if not pkgs:
6081
fail(path, "no packages recorded")
61-
print(f"OK [{path}]: {ver}, {len(d['packages'])} package(s)")
82+
for dep_name in require_deps:
83+
matches = [p for p in pkgs
84+
if dep_name.lower() in (p.get("name") or "").lower()]
85+
if not matches:
86+
fail(path, f"required dependency package matching {dep_name!r} "
87+
f"missing")
88+
if not matches[0].get("versionInfo"):
89+
fail(path, f"dependency package {matches[0].get('name')!r} has "
90+
f"no versionInfo")
91+
print(f"OK [{path}]: {ver}, {len(pkgs)} package(s)")
6292

6393

6494
def main(argv):
@@ -67,6 +97,13 @@ def main(argv):
6797
formatter_class=argparse.RawDescriptionHelpFormatter)
6898
ap.add_argument("--name-prefix", default="",
6999
help="Require metadata.component.name to start with this.")
100+
ap.add_argument("--min-properties", type=int, default=None,
101+
help="Require at least N CycloneDX component properties "
102+
"(guards empty --cflags captures).")
103+
ap.add_argument("--require-dep-version", action="append", default=[],
104+
metavar="NAME",
105+
help="Require a dependency component/package NAME with "
106+
"a non-empty version (repeatable).")
70107
ap.add_argument("files", nargs="+")
71108
args = ap.parse_args(argv[1:])
72109

@@ -79,9 +116,10 @@ def main(argv):
79116
except json.JSONDecodeError as e:
80117
fail(path, f"invalid JSON: {e}")
81118
if "bomFormat" in d or path.endswith(".cdx.json"):
82-
validate_cyclonedx(path, d, args.name_prefix)
119+
validate_cyclonedx(path, d, args.name_prefix,
120+
args.min_properties, args.require_dep_version)
83121
elif "spdxVersion" in d or path.endswith(".spdx.json"):
84-
validate_spdx(path, d)
122+
validate_spdx(path, d, args.require_dep_version)
85123
else:
86124
fail(path, "unrecognized SBOM format (neither CycloneDX nor SPDX)")
87125
print("All SBOMs valid.")

0 commit comments

Comments
 (0)