sbom: generate SBOMs from every wolfBoot build system

wolfBoot ships as source. Users build it in many ways. Before this
change, only the plain Make build could make an SBOM. So a user could not
make an SBOM for the build that the user runs.

This change adds one shared engine (tools/scripts/wolfboot-sbom.sh, which
calls wolfSSL gen-sbom) and a front end for each build system. Every
build makes a CycloneDX 1.6 and SPDX 2.3 document. The engine captures
the configuration with the host compiler, so the SBOM is the same for
GCC, Clang, LLVM, IAR, armcl, CCRX, and XC32.

Routes:
  - Make, arch.mk, and vendor SDKs: make sbom TARGET=<t> SIGN=<a>
  - CMake and the Pico SDK: cmake --build <dir> --target sbom
  - IAR Embedded Workbench: ide-sbom/iar_sbom.py
  - Any IDE with a compilation database: ide-sbom/compdb_sbom.py
  - TI CCS, MPLAB X, Renesas, Xilinx: ide-sbom/route_through_sbom.sh
  - Per-HAL component: make sbom-hal TARGET=<t>
  - Zephyr module: ide-sbom/zephyr_sbom.py

Make the SBOM reproducible. The captured macros can hold an absolute host
path. For example, arch.mk passes -DPICO_SDK_PATH=$(PICO_SDK_PATH). The
driver now redacts each absolute path but keeps the macro name, so the
configuration record stays complete. Add --no-scrub for debug.

Add a validator (ide-sbom/validate_sbom.py) and a CI canary
(.github/workflows/test-sbom.yml) that runs and validates every route.
The canary also checks that no host path leaks into the SBOM.

Add docs/SBOM.md. The tools are product-neutral by design, so they can be
shared across wolfSSL products later without logic changes.

Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
pull/824/head
Sameeh Jubran 2026-07-22 13:46:43 +03:00 committed by Daniele Lacamera
parent 6ba2172695
commit 142de77ed3
13 changed files with 1601 additions and 35 deletions

147
.github/workflows/test-sbom.yml vendored 100644
View File

@ -0,0 +1,147 @@
name: Wolfboot SBOM Canary
# Exercises every wolfBoot SBOM route so a change to the build system, the
# shared driver (tools/scripts/wolfboot-sbom.sh) or an IDE extractor cannot
# silently break SBOM generation:
#
# * Make path (methods 1-4) -> make sbom TARGET=sim
# * CMake path (method 5) -> cmake --build --target sbom
# * IAR extractor (method 7) -> tools/scripts/ide-sbom/iar_sbom.py
# * compdb extractor (6, 8-11) -> tools/scripts/ide-sbom/compdb_sbom.py
# * per-HAL SBOM -> make sbom-hal TARGET=sim
# * Zephyr module SBOM -> tools/scripts/ide-sbom/zephyr_sbom.py
#
# Each route must emit a schema-valid CycloneDX 1.6 + SPDX 2.3 document whose
# top-level component is wolfboot.
on:
push:
branches: [ 'master', 'main', 'release/**' ]
pull_request:
branches: [ '*' ]
jobs:
sbom_canary:
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@v4
with:
submodules: true
- name: Trust workspace
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"
- name: Install tooling
run: |
sudo apt-get update
sudo apt-get install -y cmake python3 build-essential
# gen-sbom ships with wolfSSL. Use the vendored submodule copy if the
# pinned revision already carries it, otherwise fetch it from wolfSSL
# master so the canary still runs while the submodule bump lands.
- name: Locate gen-sbom
id: gensbom
run: |
if [ -f lib/wolfssl/scripts/gen-sbom ]; then
echo "path=$GITHUB_WORKSPACE/lib/wolfssl/scripts/gen-sbom" >> "$GITHUB_OUTPUT"
else
mkdir -p .sbom-tools
curl -fsSL \
https://raw.githubusercontent.com/wolfSSL/wolfssl/master/scripts/gen-sbom \
-o .sbom-tools/gen-sbom
chmod +x .sbom-tools/gen-sbom
echo "path=$GITHUB_WORKSPACE/.sbom-tools/gen-sbom" >> "$GITHUB_OUTPUT"
fi
# Reproducibility guard: a -D macro carrying an absolute host path (e.g.
# arch.mk's -DPICO_SDK_PATH=$(PICO_SDK_PATH)) must never reach the SBOM.
- name: Path scrub check
run: |
printf 'src/image.c\n' > /tmp/scrub-srcs.txt
sh tools/scripts/wolfboot-sbom.sh \
--srcs-file /tmp/scrub-srcs.txt \
--cflags "-DWOLFBOOT_HASH_SHA256 -DPICO_SDK_PATH=/home/ci-secret/pico-sdk" \
--name wolfboot --version 0.0.0-scrubtest \
--gen-sbom "${{ steps.gensbom.outputs.path }}" \
--cdx-out /tmp/scrub.cdx.json --spdx-out /tmp/scrub.spdx.json
if grep -q 'ci-secret' /tmp/scrub.cdx.json /tmp/scrub.spdx.json; then
echo "ERROR: absolute host path leaked into the SBOM (scrub failed)." >&2
exit 1
fi
grep -q 'PICO_SDK_PATH' /tmp/scrub.cdx.json || {
echo "ERROR: PICO_SDK_PATH macro was dropped entirely (should be redacted, not removed)." >&2
exit 1; }
echo "scrub OK: path redacted, macro key preserved"
- name: Make path - make sbom (sim)
run: |
cp config/examples/sim.config .config
make sbom TARGET=sim GEN_SBOM="${{ steps.gensbom.outputs.path }}"
python3 tools/scripts/ide-sbom/validate_sbom.py \
wolfboot-*.cdx.json wolfboot-*.spdx.json
- name: CMake path - cmake --target sbom (sim)
run: |
rm -rf build-sim
cmake -S . -B build-sim -G "Unix Makefiles" \
-DWOLFBOOT_TARGET=sim -DARCH=HOST -DSIGN=ED25519 -DHASH=SHA256 \
-DWOLFBOOT_SECTOR_SIZE=256 -DWOLFBOOT_PARTITION_SIZE=0x6400 \
-DWOLFBOOT_PARTITION_BOOT_ADDRESS=0x08003000 \
-DWOLFBOOT_PARTITION_UPDATE_ADDRESS=0x08009400 \
-DWOLFBOOT_PARTITION_SWAP_ADDRESS=0x0800F800 \
-DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DGEN_SBOM="${{ steps.gensbom.outputs.path }}"
cmake --build build-sim --target sbom
python3 tools/scripts/ide-sbom/validate_sbom.py \
build-sim/wolfboot-*.cdx.json build-sim/wolfboot-*.spdx.json
- name: IAR extractor (method 7)
run: |
python3 tools/scripts/ide-sbom/iar_sbom.py IDE/IAR/wolfboot.ewp \
--gen-sbom "${{ steps.gensbom.outputs.path }}" \
--cdx-out iar.cdx.json --spdx-out iar.spdx.json
python3 tools/scripts/ide-sbom/validate_sbom.py iar.cdx.json iar.spdx.json
- name: compdb extractor (methods 6, 8-11)
run: |
python3 tools/scripts/ide-sbom/compdb_sbom.py \
build-sim/compile_commands.json \
--gen-sbom "${{ steps.gensbom.outputs.path }}" \
--cdx-out compdb.cdx.json --spdx-out compdb.spdx.json
python3 tools/scripts/ide-sbom/validate_sbom.py \
compdb.cdx.json compdb.spdx.json
- name: Per-HAL SBOM (make sbom-hal)
run: |
cp config/examples/sim.config .config
make sbom-hal TARGET=sim GEN_SBOM="${{ steps.gensbom.outputs.path }}"
python3 tools/scripts/ide-sbom/validate_sbom.py \
wolfboot-hal-sim-*.cdx.json wolfboot-hal-sim-*.spdx.json
- name: Zephyr module SBOM
run: |
python3 tools/scripts/ide-sbom/zephyr_sbom.py \
--gen-sbom "${{ steps.gensbom.outputs.path }}" \
--cdx-out zephyr.cdx.json --spdx-out zephyr.spdx.json
python3 tools/scripts/ide-sbom/validate_sbom.py \
zephyr.cdx.json zephyr.spdx.json
- name: Upload SBOM artifacts
if: always()
uses: actions/upload-artifact@v4
with:
name: wolfboot-sboms
path: |
wolfboot-*.cdx.json
wolfboot-*.spdx.json
build-sim/wolfboot-*.cdx.json
build-sim/wolfboot-*.spdx.json
iar.cdx.json
iar.spdx.json
compdb.cdx.json
compdb.spdx.json
zephyr.cdx.json
zephyr.spdx.json
if-no-files-found: warn

6
.gitignore vendored
View File

@ -494,3 +494,9 @@ tools/unit-tests/zynq_write_extract.h
tools/unit-tests/unit-sign-header-failure
tools/unit-tests/unit-sign-hybrid-keyload
tools/unit-tests/unit-update-ram-uboot
# Generated SBOM artifacts (CycloneDX 1.6 + SPDX 2.3)
wolfboot-*.cdx.json
wolfboot-*.spdx.json
wolfboot-*.spdx
wolfboot-sbom-srcs.txt

View File

@ -1550,4 +1550,9 @@ if(HOST_IS_MSVC) # Some VS2022 helpers
"${CMAKE_CURRENT_BINARY_DIR}")
endif() # HOST_IS_MSVC VS2022 helpers
#---------------------------------------------------------------------------------------------
# SBOM generation (CycloneDX 1.6 + SPDX 2.3), shares the engine used by `make sbom`
#---------------------------------------------------------------------------------------------
include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/sbom.cmake)
message(STATUS "End [WOLFBOOT_ROOT]/CmakeLists.txt")

View File

@ -876,10 +876,14 @@ pico-sdk-info: FORCE
# recipe echoes the effective target/sign so a default build is visible; pass
# them explicitly to get an SBOM that reflects your actual configuration.
#
# Extracts the configuration-specific source list from OBJS (which is fully
# assembled by this point — core wolfBoot + wolfcrypt + HAL sources are all
# included), captures the build's -D configuration macros via $(HOSTCC) -dM -E
# on the host, and calls gen-sbom to emit CycloneDX and SPDX output files.
# This is the plain-Make / arch.mk entry point. It also covers every build
# that is really the Makefile with a vendor SDK bolted on via source/include
# paths (MCUXpresso, STM32Cube, PSoC6, Freedom-E-SDK, Vorago) and the IDE
# targets that also have an arch.mk path (TI Hercules, Renesas RX, Zynq). It
# extracts the configuration-specific source list from OBJS (fully assembled by
# this point: core wolfBoot + wolfcrypt + HAL) and passes it, together with the
# build CFLAGS, to the shared SBOM driver (tools/scripts/wolfboot-sbom.sh) which
# is the single engine reused by the CMake and IDE entry points too.
#
# wolfcrypt sources are compiled directly into the wolfBoot image and are
# therefore listed as wolfBoot's own sources, not as a separate component.
@ -898,49 +902,64 @@ GEN_SBOM?=$(WOLFBOOT_LIB_WOLFSSL)/scripts/gen-sbom
SBOM_CDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).cdx.json
SBOM_SPDX_OUT:=wolfboot-$(WOLFBOOT_VERSION).spdx.json
SBOM_PYTHON?=$(or $(CRA_PYTHON),python3)
SBOM_DRIVER:=$(WOLFBOOT_ROOT)/tools/scripts/wolfboot-sbom.sh
sbom:
@if [ -z "$(WOLFBOOT_VERSION)" ]; then \
echo "ERROR: could not read LIBWOLFBOOT_VERSION_STRING from include/wolfboot/version.h" >&2; \
echo " (check the file exists and its version format is intact)." >&2; \
exit 1; \
fi
@if [ ! -f "$(GEN_SBOM)" ]; then \
echo "ERROR: gen-sbom not found at '$(GEN_SBOM)'." >&2; \
echo " Initialize the submodule: git submodule update --init lib/wolfssl" >&2; \
echo " or point GEN_SBOM at a wolfssl tree: make sbom GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom" >&2; \
exit 1; \
fi
@echo "wolfBoot SBOM: version=$(WOLFBOOT_VERSION) target=$(TARGET) sign=$(SIGN)"
@echo " Outputs: $(SBOM_CDX_OUT) $(SBOM_SPDX_OUT)"
$(eval _SBOM_SRCS := $(wildcard $(patsubst %.o,%.c,$(OBJS))) $(wildcard $(patsubst %.o,%.S,$(OBJS))))
@if [ -z "$(_SBOM_SRCS)" ]; then \
echo "ERROR: no source files found in OBJS — check that TARGET and SIGN are correct." >&2; \
exit 1; \
fi
@set -e; \
_dh=$$(mktemp /tmp/wolfboot-sbom-defines.XXXXXX); \
_sf=$$(mktemp /tmp/wolfboot-sbom-srcs.XXXXXX); \
trap 'rm -f "$$_dh" "$$_sf"' EXIT; \
_defs=""; \
for _t in $(CFLAGS); do \
case "$$_t" in -D*) _defs="$$_defs $$_t" ;; esac; \
done; \
$(HOSTCC) -dM -E -DWOLFSSL_USER_SETTINGS $$_defs \
-x c /dev/null >"$$_dh" 2>/dev/null || \
{ echo "ERROR: '$(HOSTCC) -dM -E' failed; install a host C compiler or set HOSTCC." >&2; exit 1; }; \
trap 'rm -f "$$_sf"' EXIT; \
printf '%s\n' $(_SBOM_SRCS) >"$$_sf"; \
$(SBOM_PYTHON) "$(GEN_SBOM)" \
"$(SBOM_DRIVER)" \
--srcs-file "$$_sf" \
--cflags "$(CFLAGS)" \
--name wolfboot \
--version "$(WOLFBOOT_VERSION)" \
--supplier "wolfSSL Inc." \
--license-file "$(WOLFBOOT_ROOT)/LICENSE" \
--options-h "$$_dh" \
--srcs-file "$$_sf" \
--gen-sbom "$(GEN_SBOM)" \
--python "$(SBOM_PYTHON)" \
--hostcc "$(HOSTCC)" \
--root "$(WOLFBOOT_ROOT)" \
--cdx-out "$(SBOM_CDX_OUT)" \
--spdx-out "$(SBOM_SPDX_OUT)"
@echo "SBOM written: $(SBOM_CDX_OUT) $(SBOM_SPDX_OUT)"
## Per-HAL SBOM
# Emits a standalone SBOM whose component is the HAL layer for the selected
# TARGET (hal/hal.c, hal/$(TARGET).c, and any target flash/uart/board drivers),
# separate from the full bootloader SBOM. Uses the same build config (CFLAGS)
# so the captured macros match the real build. Run once per TARGET.
SBOM_HAL_CDX_OUT:=wolfboot-hal-$(TARGET)-$(WOLFBOOT_VERSION).cdx.json
SBOM_HAL_SPDX_OUT:=wolfboot-hal-$(TARGET)-$(WOLFBOOT_VERSION).spdx.json
sbom-hal:
@echo "wolfBoot HAL SBOM: version=$(WOLFBOOT_VERSION) target=$(TARGET)"
$(eval _HAL_SRCS := $(filter hal/%,$(patsubst ./%,%,$(wildcard $(patsubst %.o,%.c,$(OBJS)) $(patsubst %.o,%.S,$(OBJS))))))
@if [ -z "$(_HAL_SRCS)" ]; then \
echo "ERROR: no HAL sources found in OBJS for TARGET=$(TARGET)." >&2; \
exit 1; \
fi
@set -e; \
_sf=$$(mktemp /tmp/wolfboot-hal-sbom-srcs.XXXXXX); \
trap 'rm -f "$$_sf"' EXIT; \
printf '%s\n' $(_HAL_SRCS) >"$$_sf"; \
"$(SBOM_DRIVER)" \
--srcs-file "$$_sf" \
--cflags "$(CFLAGS)" \
--name "wolfboot-hal-$(TARGET)" \
--version "$(WOLFBOOT_VERSION)" \
--license-file "$(WOLFBOOT_ROOT)/LICENSE" \
--gen-sbom "$(GEN_SBOM)" \
--python "$(SBOM_PYTHON)" \
--hostcc "$(HOSTCC)" \
--root "$(WOLFBOOT_ROOT)" \
--cdx-out "$(SBOM_HAL_CDX_OUT)" \
--spdx-out "$(SBOM_HAL_SPDX_OUT)"
FORCE:
.PHONY: FORCE clean keytool_check squashelf_check sbom
.PHONY: FORCE clean keytool_check squashelf_check sbom sbom-hal

View File

@ -142,15 +142,29 @@ make sbom TARGET=<target> SIGN=<alg> HASH=<alg>
`TARGET`, `SIGN`, and `HASH` must match your wolfBoot build configuration (same
as a normal `make` invocation), because the SBOM's source set and artifact hash
are configuration-specific. `gen-sbom` lives in the `lib/wolfssl` submodule and
is used automatically; override with `GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom`
if you keep wolfssl elsewhere.
are configuration-specific. `gen-sbom` is part of wolfSSL. The build uses the
copy in the `lib/wolfssl` submodule. If the pinned revision does not include it,
give the path with `GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom`.
The same SBOM engine is available from every wolfBoot build system, so you get
an identical CycloneDX 1.6 / SPDX 2.3 document however you build:
| Build system / artifact | How to generate the SBOM |
| --- | --- |
| Make / arch.mk / vendor SDKs | `make sbom TARGET=<target> SIGN=<alg>` |
| CMake (and Pico SDK) | `cmake --build <dir> --target sbom` |
| IAR Embedded Workbench | `tools/scripts/ide-sbom/iar_sbom.py IDE/IAR/wolfboot.ewp` |
| TI CCS / MPLAB X / Renesas / Xilinx | `tools/scripts/ide-sbom/route_through_sbom.sh --config <cfg> ...` |
| Any IDE with a compilation database | `tools/scripts/ide-sbom/compdb_sbom.py compile_commands.json` |
| Per-HAL component | `make sbom-hal TARGET=<target>` |
| Zephyr TEE/PSA module | `tools/scripts/ide-sbom/zephyr_sbom.py` |
Output files are written to the build directory as
`wolfboot-<version>.cdx.json` (CycloneDX 1.6) and `wolfboot-<version>.spdx.json`
(SPDX 2.3 JSON), where `<version>` is read from `include/wolfboot/version.h`.
For CRA guidance and worked SBOM examples, see the
See [docs/SBOM.md](./docs/SBOM.md) for the full per-build-system guide. For CRA
guidance and worked SBOM examples, see the
[wolfSSL CRA Kit](https://github.com/wolfSSL/wolfssl-examples/tree/master/cra-kit).
## Troubleshooting

124
cmake/sbom.cmake 100644
View File

@ -0,0 +1,124 @@
# cmake/sbom.cmake - CMake entry point for wolfBoot SBOM generation.
#
# This is the CMake counterpart of the Makefile `sbom` target. It reuses the
# same engine (tools/scripts/wolfboot-sbom.sh -> wolfSSL gen-sbom) so a CMake
# build and a Make build of the same configuration produce byte-comparable
# CycloneDX 1.6 and SPDX 2.3 SBOMs. It covers the CMake presets / dot-config
# builds and the Pico SDK build (which is CMake underneath).
#
# Usage:
# cmake --build <build-dir> --target sbom
#
# Overrides:
# -DGEN_SBOM=/path/to/wolfssl/scripts/gen-sbom (default: lib/wolfssl copy)
# -DHOSTCC=cc host cc for macro capture
# -DSBOM_PYTHON=python3
#
# NOTE: the driver is a POSIX shell script; on Windows run this target from a
# shell environment (WSL/MSYS/Git-Bash) or use the Makefile path.
if(NOT DEFINED WOLFBOOT_ROOT)
set(WOLFBOOT_ROOT ${CMAKE_CURRENT_SOURCE_DIR})
endif()
set(SBOM_DRIVER ${WOLFBOOT_ROOT}/tools/scripts/wolfboot-sbom.sh)
# gen-sbom: prefer an explicit override, else the vendored wolfssl submodule.
if(NOT DEFINED GEN_SBOM OR GEN_SBOM STREQUAL "")
set(GEN_SBOM ${WOLFBOOT_ROOT}/lib/wolfssl/scripts/gen-sbom)
endif()
if(NOT DEFINED HOSTCC OR HOSTCC STREQUAL "")
set(HOSTCC cc)
endif()
if(NOT DEFINED SBOM_PYTHON OR SBOM_PYTHON STREQUAL "")
set(SBOM_PYTHON python3)
endif()
# Read the wolfBoot version string from the header so CMake and Make agree.
set(_sbom_version "")
if(EXISTS ${WOLFBOOT_ROOT}/include/wolfboot/version.h)
file(STRINGS ${WOLFBOOT_ROOT}/include/wolfboot/version.h _sbom_ver_line
REGEX "LIBWOLFBOOT_VERSION_STRING")
if(_sbom_ver_line)
string(REGEX REPLACE ".*LIBWOLFBOOT_VERSION_STRING[ \t]+\"([^\"]*)\".*"
"\\1" _sbom_version "${_sbom_ver_line}")
endif()
endif()
if(_sbom_version STREQUAL "")
message(WARNING "sbom: could not read LIBWOLFBOOT_VERSION_STRING; using 0.0")
set(_sbom_version "0.0")
endif()
# Collect the compiled-in source set from the wolfBoot library targets. These
# are the same sources arch.mk folds into OBJS: core wolfBoot + HAL + keystore
# + wolfcrypt.
set(_sbom_targets wolfboot wolfboothal)
if(TARGET public_key)
list(APPEND _sbom_targets public_key)
endif()
if(DEFINED WOLFSSL_TGT AND TARGET ${WOLFSSL_TGT})
list(APPEND _sbom_targets ${WOLFSSL_TGT})
endif()
set(_sbom_srcs "")
foreach(_t IN LISTS _sbom_targets)
get_target_property(_t_srcs ${_t} SOURCES)
get_target_property(_t_dir ${_t} SOURCE_DIR)
if(_t_srcs)
foreach(_s IN LISTS _t_srcs)
# Skip generator expressions (e.g. $<TARGET_OBJECTS:...>) which are
# not plain file paths; wolfBoot's targets use plain sources.
# Also skip headers so the SBOM lists only compiled translation
# units, matching the Make path (OBJS -> .c/.S only).
if(NOT _s MATCHES "\\$<" AND _s MATCHES "\\.(c|cc|cpp|cxx|s|S|asm)$")
if(IS_ABSOLUTE "${_s}")
list(APPEND _sbom_srcs "${_s}")
else()
list(APPEND _sbom_srcs "${_t_dir}/${_s}")
endif()
endif()
endforeach()
endif()
endforeach()
list(REMOVE_DUPLICATES _sbom_srcs)
# Write the source list for the driver (one path per line).
set(_sbom_srcs_file ${CMAKE_CURRENT_BINARY_DIR}/wolfboot-sbom-srcs.txt)
string(REPLACE ";" "\n" _sbom_srcs_nl "${_sbom_srcs}")
file(GENERATE OUTPUT ${_sbom_srcs_file} CONTENT "${_sbom_srcs_nl}\n")
# Assemble the effective configuration as -D flags. The driver runs these
# through the HOST compiler's -dM -E, exactly like the Make path does with
# CFLAGS, so the captured macro set (and therefore the SBOM) is identical.
set(_sbom_defs ${WOLFBOOT_DEFS} ${WOLFBOOT_DEFS_PUBLIC} ${USER_SETTINGS} ${SIGN_OPTIONS})
list(REMOVE_DUPLICATES _sbom_defs)
set(_sbom_cflags "")
foreach(_d IN LISTS _sbom_defs)
if(NOT _d STREQUAL "")
string(REGEX REPLACE "^-D" "" _d "${_d}")
set(_sbom_cflags "${_sbom_cflags} -D${_d}")
endif()
endforeach()
set(_sbom_cdx ${CMAKE_CURRENT_BINARY_DIR}/wolfboot-${_sbom_version}.cdx.json)
set(_sbom_spdx ${CMAKE_CURRENT_BINARY_DIR}/wolfboot-${_sbom_version}.spdx.json)
add_custom_target(sbom
COMMAND ${CMAKE_COMMAND} -E env HOSTCC=${HOSTCC}
${SBOM_DRIVER}
--srcs-file ${_sbom_srcs_file}
--cflags ${_sbom_cflags}
--name wolfboot
--version ${_sbom_version}
--license-file ${WOLFBOOT_ROOT}/LICENSE
--gen-sbom ${GEN_SBOM}
--python ${SBOM_PYTHON}
--hostcc ${HOSTCC}
--root ${WOLFBOOT_ROOT}
--skip-missing
--cdx-out ${_sbom_cdx}
--spdx-out ${_sbom_spdx}
WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR}
VERBATIM
COMMENT "Generating wolfBoot SBOM (CycloneDX 1.6 + SPDX 2.3)"
)

292
docs/SBOM.md 100644
View File

@ -0,0 +1,292 @@
# wolfBoot SBOM Generation
wolfBoot can emit a Software Bill of Materials (SBOM) in **CycloneDX 1.6** and
**SPDX 2.3** JSON for every configuration and every build system it supports.
An SBOM is one of the software-transparency artifacts useful towards EU Cyber
Resilience Act (CRA) obligations; it does not by itself make a product CRA
compliant (that is a system- and process-level determination for the
manufacturer).
## One engine, many front ends
There is a single SBOM engine. Every build system feeds it the same two inputs
and gets back the same document:
```
build system ─┐
├─► tools/scripts/wolfboot-sbom.sh ─► wolfSSL gen-sbom ─► *.cdx.json + *.spdx.json
extractor ─┘ (srcs list + build config)
```
* **srcs list** the source files actually compiled into the image.
* **build config** the effective `-D` macros, normalized through the *host*
compiler's `-dM -E`. Because macro capture uses the host compiler (never the
cross-compiler), the SBOM is reproducible across toolchains: GCC, Clang/LLVM,
IAR `iccarm`, TI `armcl`, Renesas `ccrx` and Microchip `xc32` all converge to
the same document for the same configuration.
The pieces:
| File | Role |
| --- | --- |
| `tools/scripts/wolfboot-sbom.sh` | Canonical driver (srcs + config → gen-sbom). |
| `cmake/sbom.cmake` | CMake `sbom` target. |
| `tools/scripts/ide-sbom/iar_sbom.py` | Extracts srcs + defines from an IAR `.ewp`. |
| `tools/scripts/ide-sbom/compdb_sbom.py` | Extracts srcs + defines from a `compile_commands.json`. |
| `tools/scripts/ide-sbom/zephyr_sbom.py` | Extracts the Zephyr module sources from `zephyr/CMakeLists.txt`. |
| `tools/scripts/ide-sbom/route_through_sbom.sh` | Stages a config and runs `make sbom` for IDE targets that build through the Makefile. |
| `tools/scripts/ide-sbom/validate_sbom.py` | Structural sanity check used by CI. |
| `make sbom-hal` | Standalone SBOM for the HAL of a given target. |
## Prerequisites
* `python3`
* A host C compiler. The default is `cc`. To use a different compiler, set
`HOSTCC=...`.
* `gen-sbom`. This tool is part of wolfSSL. The build uses the copy in the
`lib/wolfssl` submodule. The pinned wolfSSL revision does not include
`gen-sbom` yet. Until a wolfSSL update adds it, give the path to a copy. Use
`GEN_SBOM=/path/to/wolfssl/scripts/gen-sbom` for Make and route-through. Use
`-DGEN_SBOM=...` for CMake. Use `--gen-sbom ...` for the Python tools.
```sh
git submodule update --init lib/wolfssl
```
## Limitations
Obey these limitations when you make an SBOM.
- `gen-sbom` is necessary. If the build does not find the tool, it stops and
shows an error. Give the path with `GEN_SBOM` or the equivalent option. A
wolfSSL submodule update removes this step.
- A vendor SDK build lists only the source files that are on disk. If the SDK
is not in the source tree, the SBOM does not include the SDK files. The SBOM
always includes the wolfBoot, wolfCrypt, and HAL files.
- The driver is a POSIX shell script. On Windows, run the tools in a POSIX
shell. Use WSL, MSYS, or Git Bash. As an alternative, use the compilation
database tool (`compdb_sbom.py`).
## Coverage: the 11 build methods
wolfBoot is built in many ways. Each maps to one of four SBOM routes:
| # | Build method | SBOM route |
| --- | --- | --- |
| 1 | Plain Make / `arch.mk` | Make target |
| 2 | Make + MCUXpresso SDK | Make target |
| 3 | Make + STM32Cube | Make target |
| 4 | Make + PSoC6 / Freedom-E / Vorago SDKs | Make target |
| 5 | CMake (presets / dot-config) | CMake target |
| 6 | Pico SDK (RP2350) | compdb extractor |
| 7 | IAR Embedded Workbench | IAR extractor |
| 8 | TI Code Composer Studio (Hercules TMS570) | route-through Make (or compdb) |
| 9 | Microchip MPLAB X (SAME51 / PIC32) | route-through Make (or compdb) |
| 10 | Renesas e² studio (RX / RA / RZ) | route-through Make (or compdb) |
| 11 | Xilinx SDK / Vitis (Zynq / ZynqMP) | route-through Make (or compdb) |
---
## Route 1 — Make (methods 14)
The Makefile `sbom` target is the primary entry point. It works for the plain
`arch.mk` build and for every build that is really the Makefile with a vendor
SDK bolted on via source/include paths (MCUXpresso, STM32Cube, PSoC6,
Freedom-E-SDK, Vorago).
```sh
make sbom TARGET=<target> SIGN=<alg> HASH=<alg>
```
`TARGET`, `SIGN`, and `HASH` come from the same place as a normal build (command
line, environment, or `.config`) and must match the configuration you ship —
the source set and artifact hash are configuration-specific.
Useful overrides: `HOSTCC`, `GEN_SBOM`, `CRA_PYTHON`.
wolfcrypt sources are compiled directly into the wolfBoot image, so they are
listed as wolfBoot's own sources rather than as a separate component.
## Route 2 — CMake (methods 56)
The CMake build exposes an `sbom` target (`cmake/sbom.cmake`) that collects the
compiled source set from the wolfBoot library targets and the effective
configuration from `WOLFBOOT_DEFS` / `USER_SETTINGS`, then calls the shared
driver — producing a document byte-comparable with the Make path.
```sh
cmake -S . -B build-sim -DWOLFBOOT_TARGET=sim ... # your normal configure
cmake --build build-sim --target sbom
```
Outputs land in the build directory. Overrides: `-DGEN_SBOM=...`, `-DHOSTCC=...`,
`-DSBOM_PYTHON=...`.
> The driver is a POSIX shell script; on Windows run this target from WSL / MSYS
> / Git-Bash, or use the Make path.
### Pico SDK (method 6)
The Pico SDK build under `IDE/pico-sdk/rp2350/` is a standalone CMake project
that pulls in the Pico SDK, so it does not include `cmake/sbom.cmake`. Generate
its SBOM from the compilation database (see Route 4):
```sh
cd IDE/pico-sdk/rp2350/wolfboot
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ... # your normal configure
python3 <wolfboot>/tools/scripts/ide-sbom/compdb_sbom.py build/compile_commands.json
```
## Route 3 — IAR extractor (method 7)
IAR builds happen entirely inside Embedded Workbench and never touch the
Makefile or CMake, so the compiled source set and preprocessor configuration
live in the `.ewp` project file. The extractor reads them out and feeds the
shared driver:
```sh
tools/scripts/ide-sbom/iar_sbom.py IDE/IAR/wolfboot.ewp
```
Options: `--config <name>` (defaults to the configuration with the most defines,
i.e. the real build config), `--gen-sbom`, `--version`, `--cdx-out`,
`--spdx-out`, and `--print-only` to inspect the extracted sources/defines
without generating.
Sources listed in the `.ewp` that are generated at build time (e.g.
`keystore.c`) and are not on disk are reported and excluded, matching the Make
path's `$(wildcard)` behavior.
## Route 4 — route-through & compilation database (methods 811)
### Route-through Make (preferred where a `.config` exists)
TI CCS (Hercules TMS570), Microchip MPLAB X (SAME51 / PIC32), Renesas RX, and
Xilinx Zynq / ZynqMP all have wolfBoot `config/examples/*.config` targets and
build through the Makefile on the command line. For these, the SBOM is produced
by the same `make sbom` engine; `route_through_sbom.sh` makes that explicit by
staging the config and forwarding the vendor make variables:
```sh
# TI Hercules (CCS toolchain, built from the command line):
tools/scripts/ide-sbom/route_through_sbom.sh \
--config config/examples/ti-tms570lc435.config \
CCS_ROOT=/opt/ti/ccs/tools/compiler/ti-cgt-arm_20.2.7.LTS \
F021_DIR=/opt/ti/Hercules/F021_Flash_API/02.01.01
# Xilinx ZynqMP:
tools/scripts/ide-sbom/route_through_sbom.sh --config config/examples/zynqmp.config
# Renesas RX72N:
tools/scripts/ide-sbom/route_through_sbom.sh --config config/examples/renesas-rx72n.config
# Microchip SAME51:
tools/scripts/ide-sbom/route_through_sbom.sh --config config/examples/same51.config
```
### Compilation-database extractor (any IDE / toolchain)
When a target is built *strictly inside* an IDE (e.g. Renesas RA/RZ e² studio,
an MPLAB X GUI build, or a Vitis build) and you want an SBOM of exactly what the
IDE compiled, capture a Clang compilation database and use the universal
extractor. This is toolchain- and IDE-independent:
```sh
# CMake emits it natively:
cmake -B build -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ...
# Make-based IDE projects (MPLAB X nbproject, CCS, Vitis) via Bear:
bear -- make # produces compile_commands.json
python3 tools/scripts/ide-sbom/compdb_sbom.py compile_commands.json \
--exclude 'test-app/' # optional: drop test sources
```
The extractor takes the exact file list and `-D` set the compiler saw, so the
SBOM reflects the real IDE build regardless of how sources and defines were
configured in the GUI.
## Per-artifact SBOMs
The routes above describe the wolfBoot **bootloader** image. wolfBoot also has
sub-components you may want to inventory separately. Each gets its own SBOM file
whose component is named `wolfboot-<artifact>`, so nothing collides.
### Per-HAL SBOM
The hardware abstraction layer for a target (`hal/hal.c`, `hal/<target>.c`, and
any target flash / UART / board drivers) is already included in the full
bootloader SBOM. To emit it as a **standalone** component — e.g. to track the
board-support portion of the supply chain on its own — use:
```sh
make sbom-hal TARGET=<target> SIGN=<alg>
```
This reuses the real build `CFLAGS`, so the captured configuration matches the
bootloader build. Output: `wolfboot-hal-<target>-<version>.{cdx,spdx}.json`.
Run it once per target.
### Zephyr TEE / PSA module
The `zephyr/` directory is **not** the bootloader — it is a Zephyr module that
compiles a small TEE/PSA non-secure client shim into a Zephyr application
(`zephyr_library_sources(...)`, gated on `CONFIG_WOLFBOOT_TEE`). It is built by
Zephyr/west, so neither the Make nor the CMake SBOM target sees it. The
extractor reads the module's source list straight from `zephyr/CMakeLists.txt`
(staying in sync automatically):
```sh
tools/scripts/ide-sbom/zephyr_sbom.py
```
Output: `wolfboot-zephyr-<version>.{cdx,spdx}.json`.
Because the module's configuration is Kconfig-driven (`CONFIG_*` symbols) rather
than a `-D` macro set, this is a **source-inventory** SBOM by default (no
build-config macros; the driver's `--source-only` mode). If you have a real
Zephyr build and want the exact compiled configuration, generate the SBOM from
that build's compilation database instead:
```sh
west build ... -- -DCMAKE_EXPORT_COMPILE_COMMANDS=ON
python3 tools/scripts/ide-sbom/compdb_sbom.py build/compile_commands.json \
--include 'zephyr/src/' --name wolfboot-zephyr
```
## Output
Every route writes, into the working/build directory:
* `wolfboot-<version>.cdx.json` — CycloneDX 1.6
* `wolfboot-<version>.spdx.json` — SPDX 2.3
`<version>` is read from `include/wolfboot/version.h`. These are ignored by
`.gitignore`.
You can sanity-check any output:
```sh
python3 tools/scripts/ide-sbom/validate_sbom.py wolfboot-*.cdx.json wolfboot-*.spdx.json
```
## Continuous integration
`.github/workflows/test-sbom.yml` is an SBOM canary that runs the Make, CMake,
IAR, and compilation-database routes on every push/PR and validates each output,
so a change to a build system, the shared driver, or an extractor cannot
silently break SBOM generation. The generated SBOMs are uploaded as build
artifacts.
## Reproducibility
`gen-sbom` supports deterministic output (e.g. `SOURCE_DATE_EPOCH` and stable
UUIDs). Combined with host-compiler macro capture, the same wolfBoot
configuration yields the same SBOM regardless of the build system or
cross-toolchain used to produce the firmware.
The driver also scrubs absolute host paths from the captured macros. For
example, `arch.mk` passes `-DPICO_SDK_PATH=$(PICO_SDK_PATH)`. Without the scrub,
the local path enters the SBOM. This makes the SBOM machine-specific and leaks
the local file system. The driver redacts the path but keeps the macro name, so
the configuration record stays complete. Use `--no-scrub` for debug only.

View File

@ -0,0 +1,176 @@
#!/usr/bin/env python3
"""Generate a wolfBoot SBOM from a Clang compilation database (compile_commands.json).
This is the universal IDE/toolchain fallback. Any build that can emit a
compilation database gives us an exact, ground-truth list of the files that were
compiled and the -D configuration they were compiled with - independent of the
build system or compiler. That covers cases with no Make/CMake SBOM path:
* CMake builds (configure with -DCMAKE_EXPORT_COMPILE_COMMANDS=ON)
* TI Code Composer Studio (CCS / armcl, via `bear -- make ...`)
* Microchip MPLAB X (xc32, via `bear -- make ...` on the nbproject make)
* Renesas e2studio / CCRX (via a build wrapper that records commands)
* Xilinx SDK / Vitis (via `bear -- make ...`)
The extracted sources + defines are handed to the shared driver
(tools/scripts/wolfboot-sbom.sh), so the resulting CycloneDX 1.6 / SPDX 2.3
SBOM is identical in shape to the Make, CMake and IAR paths.
Usage:
tools/scripts/ide-sbom/compdb_sbom.py build/compile_commands.json [options]
Options:
--include REGEX Only include source files whose absolute path matches REGEX
(repeatable). Default: all C/asm sources.
--exclude REGEX Exclude source files whose absolute path matches REGEX
(repeatable, applied after --include). Handy to drop
test-app/ or unit-test sources.
--gen-sbom PATH Path to wolfSSL scripts/gen-sbom (passed through).
--version VER Package version (passed through).
--cdx-out / --spdx-out PATH Output paths (passed through).
--srcs-out PATH Where to write the extracted source list (default: temp).
--print-only Print the extracted sources + defines and exit.
"""
import argparse
import json
import os
import re
import shlex
import subprocess
import sys
import tempfile
SRC_EXTS = ('.c', '.cc', '.cpp', '.cxx', '.s', '.asm')
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', '..'))
DRIVER = os.path.join(ROOT, 'tools', 'scripts', 'wolfboot-sbom.sh')
def entry_tokens(entry):
"""Return the compiler argument tokens for a compdb entry."""
if 'arguments' in entry and entry['arguments']:
return list(entry['arguments'])
if 'command' in entry and entry['command']:
try:
return shlex.split(entry['command'])
except ValueError:
return entry['command'].split()
return []
def extract_defines(tokens):
"""Return -D define tokens (normalized to bare NAME[=VAL]) from arg tokens."""
defs = []
i = 0
while i < len(tokens):
t = tokens[i]
if t == '-D' and i + 1 < len(tokens):
defs.append(tokens[i + 1])
i += 2
continue
if t.startswith('-D'):
defs.append(t[2:])
i += 1
return defs
def main():
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('compdb', help='Path to compile_commands.json')
ap.add_argument('--include', action='append', default=[])
ap.add_argument('--exclude', action='append', default=[])
ap.add_argument('--gen-sbom', default=None)
ap.add_argument('--version', default=None)
ap.add_argument('--name', default='wolfboot')
ap.add_argument('--cdx-out', default=None)
ap.add_argument('--spdx-out', default=None)
ap.add_argument('--srcs-out', default=None)
ap.add_argument('--print-only', action='store_true')
args = ap.parse_args()
if not os.path.isfile(args.compdb):
sys.exit(f"ERROR: compilation database not found: {args.compdb}")
with open(args.compdb) as f:
try:
db = json.load(f)
except json.JSONDecodeError as e:
sys.exit(f"ERROR: cannot parse {args.compdb}: {e}")
inc = [re.compile(p) for p in args.include]
exc = [re.compile(p) for p in args.exclude]
srcs = []
defines = set()
seen = set()
for entry in db:
fpath = entry.get('file')
if not fpath:
continue
directory = entry.get('directory', os.getcwd())
if not os.path.isabs(fpath):
fpath = os.path.join(directory, fpath)
fpath = os.path.normpath(fpath)
if not fpath.lower().endswith(SRC_EXTS):
continue
if inc and not any(r.search(fpath) for r in inc):
continue
if exc and any(r.search(fpath) for r in exc):
continue
tokens = entry_tokens(entry)
for d in extract_defines(tokens):
defines.add(d)
if fpath not in seen and os.path.isfile(fpath):
seen.add(fpath)
srcs.append(fpath)
if not srcs:
sys.exit("ERROR: no matching source files found in compilation database")
defines = sorted(defines)
cflags = ' '.join(f'-D{d}' for d in defines)
if args.print_only:
print(f"# {len(srcs)} sources, {len(defines)} defines")
print("\n[defines]")
for d in defines:
print(f" -D{d}")
print("\n[sources]")
for s in srcs:
print(f" {s}")
return
srcs_out = args.srcs_out
tmp = None
if not srcs_out:
fd, srcs_out = tempfile.mkstemp(prefix='wolfboot-compdb-srcs-', suffix='.txt')
os.close(fd)
tmp = srcs_out
with open(srcs_out, 'w') as f:
f.write('\n'.join(srcs) + '\n')
cmd = [DRIVER, '--srcs-file', srcs_out, '--cflags', cflags,
'--name', args.name, '--root', ROOT]
if args.version:
cmd += ['--version', args.version]
if args.gen_sbom:
cmd += ['--gen-sbom', args.gen_sbom]
if args.cdx_out:
cmd += ['--cdx-out', args.cdx_out]
if args.spdx_out:
cmd += ['--spdx-out', args.spdx_out]
print(f"compdb SBOM: {len(srcs)} sources, {len(defines)} defines")
try:
rc = subprocess.call(cmd)
finally:
if tmp and os.path.exists(tmp):
os.remove(tmp)
sys.exit(rc)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,198 @@
#!/usr/bin/env python3
"""Generate a wolfBoot SBOM from an IAR Embedded Workbench project (.ewp).
IAR builds happen entirely inside the IDE and never touch the Makefile or CMake,
so the compiled source set and the preprocessor configuration live in the .ewp
project file rather than in OBJS / CFLAGS. This extractor reads them out of the
.ewp and feeds them to the shared wolfBoot SBOM driver
(tools/scripts/wolfboot-sbom.sh), so an IAR-built wolfBoot gets the same
CycloneDX 1.6 + SPDX 2.3 SBOM as a Make- or CMake-built one.
It parses:
* the C compiler preprocessor defines (ICCARM -> CCDefines <state> entries)
* the compiled source files (<file><name> ...), resolving $PROJ_DIR$
Usage:
tools/scripts/ide-sbom/iar_sbom.py IDE/IAR/wolfboot.ewp [options]
Options:
--config NAME IAR build configuration to read (default: the configuration
with the most preprocessor defines, i.e. the real one).
--gen-sbom PATH Path to wolfSSL scripts/gen-sbom (passed through).
--version VER Package version (passed through; else driver reads version.h).
--cdx-out PATH / --spdx-out PATH Output paths (passed through).
--srcs-out PATH Where to write the extracted source list
(default: a temp file).
--print-only Print the extracted sources + defines and exit (no SBOM).
"""
import argparse
import os
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as ET
SRC_EXTS = ('.c', '.cc', '.cpp', '.cxx', '.s', '.asm')
# Repo root: tools/scripts/ide-sbom/iar_sbom.py -> up 3.
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', '..'))
DRIVER = os.path.join(ROOT, 'tools', 'scripts', 'wolfboot-sbom.sh')
def resolve_proj_dir(raw, proj_dir):
"""Resolve an IAR $PROJ_DIR$-relative, backslash path to an absolute path."""
p = raw.replace('$PROJ_DIR$', proj_dir)
p = p.replace('\\', '/')
if not os.path.isabs(p):
p = os.path.join(proj_dir, p)
return os.path.normpath(p)
def parse_configs(root):
"""Return {config_name: [define, ...]} from each configuration's ICCARM
CCDefines option."""
configs = {}
for cfg in root.findall('configuration'):
name_el = cfg.find('name')
cfg_name = name_el.text if name_el is not None else '(unnamed)'
defines = []
for settings in cfg.findall('settings'):
sname = settings.find('name')
if sname is None or sname.text != 'ICCARM':
continue
for data in settings.findall('data'):
for option in data.findall('option'):
oname = option.find('name')
if oname is None or oname.text != 'CCDefines':
continue
for state in option.findall('state'):
if state.text:
defines.append(state.text.strip())
configs[cfg_name] = defines
return configs
def collect_sources(root, proj_dir):
"""Return (present, missing) absolute paths of compiled source files.
Files that do not exist on disk are separated out: build-time generated
files such as keystore.c are listed in the .ewp but only materialize during
a build. This mirrors the Make path, where $(wildcard) silently drops
sources that are not present.
"""
srcs = []
for file_el in root.iter('file'):
name_el = file_el.find('name')
if name_el is None or not name_el.text:
continue
raw = name_el.text.strip()
if not raw.lower().endswith(SRC_EXTS):
continue
srcs.append(resolve_proj_dir(raw, proj_dir))
# De-dup while preserving order, then split on existence.
seen = set()
present, missing = [], []
for s in srcs:
if s in seen:
continue
seen.add(s)
(present if os.path.isfile(s) else missing).append(s)
return present, missing
def main():
ap = argparse.ArgumentParser(description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('ewp', help='Path to the IAR .ewp project file')
ap.add_argument('--config', help='IAR configuration name (default: richest)')
ap.add_argument('--gen-sbom', default=None)
ap.add_argument('--version', default=None)
ap.add_argument('--cdx-out', default=None)
ap.add_argument('--spdx-out', default=None)
ap.add_argument('--srcs-out', default=None)
ap.add_argument('--print-only', action='store_true')
args = ap.parse_args()
if not os.path.isfile(args.ewp):
sys.exit(f"ERROR: .ewp not found: {args.ewp}")
proj_dir = os.path.dirname(os.path.abspath(args.ewp))
try:
tree = ET.parse(args.ewp)
except ET.ParseError as e:
sys.exit(f"ERROR: cannot parse {args.ewp}: {e}")
root = tree.getroot()
configs = parse_configs(root)
if not configs:
sys.exit("ERROR: no <configuration> with ICCARM CCDefines found in .ewp")
if args.config:
if args.config not in configs:
sys.exit(f"ERROR: config {args.config!r} not found. "
f"Available: {', '.join(configs)}")
cfg_name = args.config
else:
# Pick the configuration with the most defines (the real build config;
# a Debug config often only carries NDEBUG-style noise).
cfg_name = max(configs, key=lambda k: len(configs[k]))
defines = configs[cfg_name]
srcs, missing = collect_sources(root, proj_dir)
if not srcs:
sys.exit("ERROR: no existing source files found in .ewp")
if missing:
sys.stderr.write(
"WARNING: %d source(s) listed in the .ewp were not found on disk "
"and are excluded (e.g. build-time generated files like "
"keystore.c):\n" % len(missing))
for m in missing:
sys.stderr.write(" - %s\n" % m)
cflags = ' '.join(f'-D{d}' for d in defines)
if args.print_only:
print(f"# IAR configuration: {cfg_name}")
print(f"# {len(srcs)} sources, {len(defines)} defines")
print("\n[defines]")
for d in defines:
print(f" -D{d}")
print("\n[sources]")
for s in srcs:
print(f" {s}")
return
srcs_out = args.srcs_out
tmp = None
if not srcs_out:
fd, srcs_out = tempfile.mkstemp(prefix='wolfboot-iar-srcs-', suffix='.txt')
os.close(fd)
tmp = srcs_out
with open(srcs_out, 'w') as f:
f.write('\n'.join(srcs) + '\n')
cmd = [DRIVER, '--srcs-file', srcs_out, '--cflags', cflags,
'--name', 'wolfboot', '--root', ROOT]
if args.version:
cmd += ['--version', args.version]
if args.gen_sbom:
cmd += ['--gen-sbom', args.gen_sbom]
if args.cdx_out:
cmd += ['--cdx-out', args.cdx_out]
if args.spdx_out:
cmd += ['--spdx-out', args.spdx_out]
print(f"IAR SBOM: configuration={cfg_name} "
f"({len(srcs)} sources, {len(defines)} defines)")
try:
rc = subprocess.call(cmd)
finally:
if tmp and os.path.exists(tmp):
os.remove(tmp)
sys.exit(rc)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,64 @@
#!/bin/sh
# route_through_sbom.sh - SBOM for IDE/SDK targets that build through the Makefile.
#
# Several "IDE" ecosystems supported by wolfBoot are, on the command line, just
# the normal Makefile build with a vendor toolchain and a target .config:
#
# * TI Code Composer Studio (Hercules TMS570) -> make CCS_ROOT=.. F021_DIR=..
# * Xilinx SDK / Vitis (Zynq / ZynqMP) -> make TARGET=zynqmp ..
# * Renesas RX (e2studio) via CCRX -> make TARGET=rx72n RX_GCC..=..
# * Microchip MPLAB X (nbproject makefiles) -> make (generated makefile)
#
# For those, the SBOM is produced by the same `make sbom` engine as any other
# Make build - this wrapper just makes that explicit and self-documenting by
# staging a config and forwarding the vendor make variables.
#
# Usage:
# route_through_sbom.sh --config config/examples/<file>.config [MAKE_VAR=VAL ...]
# route_through_sbom.sh --no-config [MAKE_VAR=VAL ...]
#
# Everything after the flags is passed verbatim to `make sbom`, so the vendor
# variables (CCS_ROOT, F021_DIR, TARGET, ...) and SBOM overrides (GEN_SBOM,
# HOSTCC, ...) all work.
#
# Examples:
# # TI Hercules (CCS toolchain, built from the command line):
# route_through_sbom.sh --config config/examples/ti-tms570lc435.config \
# CCS_ROOT=/opt/ti/ccs/tools/compiler/ti-cgt-arm_20.2.7.LTS \
# F021_DIR=/opt/ti/Hercules/F021_Flash_API/02.01.01
#
# # Xilinx ZynqMP:
# route_through_sbom.sh --config config/examples/zynqmp.config
set -e
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../../.." && pwd)
CONFIG=""
NO_CONFIG=0
while [ $# -gt 0 ]; do
case "$1" in
--config) CONFIG="$2"; shift 2 ;;
--no-config) NO_CONFIG=1; shift ;;
-h|--help) sed -n '2,40p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
*) break ;;
esac
done
cd "$ROOT"
if [ "$NO_CONFIG" -ne 1 ]; then
if [ -z "$CONFIG" ]; then
echo "ERROR: pass --config <file> (or --no-config to use the current .config)." >&2
exit 2
fi
if [ ! -f "$CONFIG" ]; then
echo "ERROR: config not found: $CONFIG" >&2
exit 1
fi
echo "Staging $CONFIG -> .config"
cp "$CONFIG" .config
fi
echo "Running: make sbom $*"
exec make sbom "$@"

View File

@ -0,0 +1,89 @@
#!/usr/bin/env python3
"""Lightweight structural validator for wolfBoot SBOM output.
Not a full schema validator - it asserts the essentials that every wolfBoot
SBOM route must satisfy, so CI (and humans) can fail fast on a broken generator:
CycloneDX (*.cdx.json):
* bomFormat == "CycloneDX"
* specVersion == "1.6"
* metadata.component.name == "wolfboot"
* metadata.component has a non-empty version
* at least one component or source recorded
SPDX (*.spdx.json):
* spdxVersion starts with "SPDX-2"
* has a name and at least one package
The file kind is detected by content, so argument order does not matter.
Usage:
validate_sbom.py FILE [FILE ...]
"""
import json
import sys
# Every wolfBoot artifact SBOM names its component in the wolfboot* family:
# wolfboot, wolfboot-hal-<target>, wolfboot-zephyr, ...
EXPECTED_NAME_PREFIX = "wolfboot"
def fail(path, msg):
print(f"FAIL [{path}]: {msg}", file=sys.stderr)
sys.exit(1)
def validate_cyclonedx(path, d):
if d.get("bomFormat") != "CycloneDX":
fail(path, f"bomFormat != CycloneDX (got {d.get('bomFormat')!r})")
if d.get("specVersion") != "1.6":
fail(path, f"specVersion != 1.6 (got {d.get('specVersion')!r})")
comp = d.get("metadata", {}).get("component", {})
name = comp.get("name", "")
if not name.startswith(EXPECTED_NAME_PREFIX):
fail(path, f"metadata.component.name does not start with "
f"{EXPECTED_NAME_PREFIX!r} (got {name!r})")
if not comp.get("version"):
fail(path, "metadata.component.version is empty")
# Sources are recorded as sub-components and/or properties; require some.
if not d.get("components") and not comp.get("properties"):
fail(path, "no components or component properties recorded")
print(f"OK [{path}]: CycloneDX 1.6, component "
f"{comp['name']} {comp['version']}")
def validate_spdx(path, d):
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"):
fail(path, "no packages recorded")
print(f"OK [{path}]: {ver}, {len(d['packages'])} package(s)")
def main(argv):
if len(argv) < 2:
print(__doc__)
sys.exit(2)
for path in argv[1:]:
try:
with open(path) as f:
d = json.load(f)
except FileNotFoundError:
fail(path, "file not found")
except json.JSONDecodeError as e:
fail(path, f"invalid JSON: {e}")
if "bomFormat" in d or path.endswith(".cdx.json"):
validate_cyclonedx(path, d)
elif "spdxVersion" in d or path.endswith(".spdx.json"):
validate_spdx(path, d)
else:
fail(path, "unrecognized SBOM format (neither CycloneDX nor SPDX)")
print("All SBOMs valid.")
if __name__ == "__main__":
main(sys.argv)

View File

@ -0,0 +1,145 @@
#!/usr/bin/env python3
"""Generate an SBOM for the wolfBoot Zephyr module (TEE / PSA client).
The `zephyr/` directory is not the bootloader: it is a Zephyr module that
compiles a small TEE/PSA non-secure client shim *into a Zephyr application*
(`zephyr_library_sources(...)`, gated on CONFIG_WOLFBOOT_TEE). Its source set is
fixed and lives in the module's CMakeLists, but it is built by Zephyr/west - not
by wolfBoot's Makefile or CMakeLists - so neither the Make nor the CMake SBOM
target sees it.
This extractor reads the module's source list straight out of
`zephyr/CMakeLists.txt` (so it stays in sync automatically) and hands it to the
shared driver as a separate component, `wolfboot-zephyr`.
The module's configuration is Kconfig-driven (CONFIG_* symbols), not a `-D`
macro set, so by default this produces a source-inventory SBOM (no build-config
macros). If you have a real Zephyr build and want the exact compiled config,
generate the SBOM from that build's compilation database with compdb_sbom.py
instead.
Usage:
tools/scripts/ide-sbom/zephyr_sbom.py [--cmakelists zephyr/CMakeLists.txt] [options]
Options:
--cmakelists PATH Module CMakeLists (default: <root>/zephyr/CMakeLists.txt).
--gen-sbom PATH Path to wolfSSL scripts/gen-sbom (passed through).
--version VER Package version (passed through).
--cdx-out / --spdx-out PATH Output paths
(default: wolfboot-zephyr-<version>.{cdx,spdx}.json).
--srcs-out PATH Where to write the extracted source list (default: temp).
--print-only Print the extracted sources and exit.
"""
import argparse
import os
import re
import subprocess
import sys
import tempfile
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(SCRIPT_DIR, '..', '..', '..'))
DRIVER = os.path.join(ROOT, 'tools', 'scripts', 'wolfboot-sbom.sh')
SRC_EXTS = ('.c', '.cc', '.cpp', '.cxx', '.s', '.asm')
def parse_library_sources(cmakelists):
"""Extract the file list from the zephyr_library_sources(...) block."""
with open(cmakelists) as f:
text = f.read()
module_dir = os.path.dirname(os.path.abspath(cmakelists))
srcs = []
for m in re.finditer(r'zephyr_library_sources\s*\((.*?)\)', text, re.DOTALL):
for raw in m.group(1).split():
raw = raw.strip()
if not raw or not raw.lower().endswith(SRC_EXTS):
continue
# Resolve the CMake variables Zephyr uses for the module directory.
p = raw.replace('${CMAKE_CURRENT_LIST_DIR}', module_dir)
p = p.replace('${CMAKE_CURRENT_SOURCE_DIR}', module_dir)
p = p.replace('${WOLFBOOT_MODULE_DIR}', os.path.dirname(module_dir))
if '${' in p:
# Unknown variable - skip rather than emit a bogus path.
sys.stderr.write(f"WARNING: skipping unresolved source: {raw}\n")
continue
if not os.path.isabs(p):
p = os.path.join(module_dir, p)
srcs.append(os.path.normpath(p))
seen = set()
present, missing = [], []
for s in srcs:
if s in seen:
continue
seen.add(s)
(present if os.path.isfile(s) else missing).append(s)
return present, missing
def main():
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument('--cmakelists', default=os.path.join(ROOT, 'zephyr', 'CMakeLists.txt'))
ap.add_argument('--gen-sbom', default=None)
ap.add_argument('--version', default=None)
ap.add_argument('--cdx-out', default=None)
ap.add_argument('--spdx-out', default=None)
ap.add_argument('--srcs-out', default=None)
ap.add_argument('--print-only', action='store_true')
args = ap.parse_args()
if not os.path.isfile(args.cmakelists):
sys.exit(f"ERROR: CMakeLists not found: {args.cmakelists}")
srcs, missing = parse_library_sources(args.cmakelists)
if missing:
sys.stderr.write(
"WARNING: %d module source(s) not found on disk (excluded):\n"
% len(missing))
for m in missing:
sys.stderr.write(" - %s\n" % m)
if not srcs:
sys.exit("ERROR: no wolfBoot Zephyr module sources found in "
+ args.cmakelists)
if args.print_only:
print(f"# wolfboot-zephyr: {len(srcs)} sources")
for s in srcs:
print(f" {s}")
return
srcs_out = args.srcs_out
tmp = None
if not srcs_out:
fd, srcs_out = tempfile.mkstemp(prefix='wolfboot-zephyr-srcs-', suffix='.txt')
os.close(fd)
tmp = srcs_out
with open(srcs_out, 'w') as f:
f.write('\n'.join(srcs) + '\n')
cmd = [DRIVER, '--srcs-file', srcs_out, '--source-only',
'--name', 'wolfboot-zephyr', '--root', ROOT]
if args.version:
cmd += ['--version', args.version]
if args.gen_sbom:
cmd += ['--gen-sbom', args.gen_sbom]
if args.cdx_out:
cmd += ['--cdx-out', args.cdx_out]
if args.spdx_out:
cmd += ['--spdx-out', args.spdx_out]
print(f"Zephyr module SBOM: {len(srcs)} sources (source-inventory)")
try:
rc = subprocess.call(cmd)
finally:
if tmp and os.path.exists(tmp):
os.remove(tmp)
sys.exit(rc)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,287 @@
#!/bin/sh
# wolfboot-sbom.sh - canonical wolfBoot SBOM driver.
#
# One engine, reused by every wolfBoot build system (plain Make/arch.mk, the
# vendor-SDK Make wrappers, CMake, the Pico SDK, and the IDE extractors) so the
# generated CycloneDX 1.6 + SPDX 2.3 SBOM is identical no matter how wolfBoot
# was built. Each build system only has to produce two things and hand them to
# this script:
#
# 1. the list of source files actually compiled into the image (--srcs-file)
# 2. the effective build configuration, either as raw build CFLAGS (--cflags)
# or as a pre-expanded flat #define header (--options-h)
#
# The heavy lifting (config-macro normalization, hashing, schema-shaped output)
# is done by wolfSSL's product-agnostic gen-sbom. The macro capture runs the
# HOST compiler (never the cross-compiler), so the SBOM is byte-reproducible
# across toolchains: gcc, clang/LLVM, IAR, armcl, CCRX and XC32 all converge to
# the same document for the same configuration.
#
# Reproducibility: captured macros are scrubbed of absolute host paths before
# they reach gen-sbom (e.g. -DPICO_SDK_PATH=/home/you/pico-sdk from arch.mk).
# An unscrubbed path makes the SBOM machine-specific and leaks the local file
# system into a published artifact. Use --no-scrub to disable (debug only).
#
# PORTABILITY NOTE
# This script is product-neutral by design (name, version, root, gen-sbom and
# license are all arguments; nothing wolfBoot-specific is hard-coded). The same
# driver can therefore be reused by other products; only the caller passes
# different --root/--name values, and the logic is unchanged.
#
# Exit status is non-zero on any error.
set -e
usage() {
cat >&2 <<'EOF'
Usage: wolfboot-sbom.sh --srcs-file PATH (--cflags "..." | --options-h PATH) [options]
Required:
--srcs-file PATH File listing compiled-in sources, one path per line
(blank lines and lines starting with # are ignored).
Configuration source (exactly one):
--cflags "..." Build CFLAGS. -D tokens are extracted and expanded through
$HOSTCC -dM -E to capture the effective wolfBoot/wolfCrypt
configuration.
--options-h PATH A pre-expanded flat #define header (e.g. output of
`$CC -dM -E`). Used verbatim; HOSTCC is not invoked.
--source-only Produce a source-inventory SBOM with no build-config
macros. Use for artifacts whose configuration is not a
`-D` set (e.g. the Zephyr module, which is Kconfig-driven).
Options:
--name NAME Package name recorded in the SBOM (default: wolfboot).
--version VER Package version (default: read from
include/wolfboot/version.h under --root).
--license-file P LICENSE file for SPDX id detection (default: <root>/LICENSE).
--cdx-out PATH CycloneDX output (default: <name>-<version>.cdx.json).
--spdx-out PATH SPDX output (default: <name>-<version>.spdx.json).
--gen-sbom PATH Path to wolfSSL scripts/gen-sbom
(default: <root>/lib/wolfssl/scripts/gen-sbom).
--python BIN Python interpreter (default: python3).
--hostcc BIN Host C compiler for macro capture (default: cc).
--root PATH wolfBoot root (default: derived from this script location).
--skip-missing Drop source paths that do not exist on disk (with a
warning) instead of failing. Mirrors the Make path, where
$(wildcard) silently omits not-yet-generated files such as
keystore.c.
--no-scrub Do not redact absolute host paths from the captured macro
header. Debug only; the output is then not reproducible.
-h, --help Show this help.
EOF
}
# Defaults.
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
ROOT=$(CDPATH= cd -- "$SCRIPT_DIR/../.." && pwd)
SRCS_FILE=""
CFLAGS_IN=""
OPTIONS_H=""
NAME="wolfboot"
VERSION=""
LICENSE_FILE=""
CDX_OUT=""
SPDX_OUT=""
GEN_SBOM=""
PYTHON="${CRA_PYTHON:-python3}"
HOSTCC="${HOSTCC:-cc}"
SKIP_MISSING=0
SOURCE_ONLY=0
SCRUB=1
while [ $# -gt 0 ]; do
case "$1" in
--srcs-file) SRCS_FILE="$2"; shift 2 ;;
--cflags) CFLAGS_IN="$2"; shift 2 ;;
--options-h) OPTIONS_H="$2"; shift 2 ;;
--source-only) SOURCE_ONLY=1; shift ;;
--name) NAME="$2"; shift 2 ;;
--version) VERSION="$2"; shift 2 ;;
--license-file) LICENSE_FILE="$2"; shift 2 ;;
--cdx-out) CDX_OUT="$2"; shift 2 ;;
--spdx-out) SPDX_OUT="$2"; shift 2 ;;
--gen-sbom) GEN_SBOM="$2"; shift 2 ;;
--python) PYTHON="$2"; shift 2 ;;
--hostcc) HOSTCC="$2"; shift 2 ;;
--root) ROOT=$(CDPATH= cd -- "$2" && pwd); shift 2 ;;
--skip-missing) SKIP_MISSING=1; shift ;;
--no-scrub) SCRUB=0; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "ERROR: unknown argument: $1" >&2; usage; exit 2 ;;
esac
done
# Resolve remaining defaults now that --root is known.
[ -n "$LICENSE_FILE" ] || LICENSE_FILE="$ROOT/LICENSE"
[ -n "$GEN_SBOM" ] || GEN_SBOM="$ROOT/lib/wolfssl/scripts/gen-sbom"
if [ -z "$VERSION" ]; then
VERSION=$(sed -n \
's/.*LIBWOLFBOOT_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
"$ROOT/include/wolfboot/version.h" 2>/dev/null || true)
fi
# Validate inputs.
if [ -z "$SRCS_FILE" ]; then
echo "ERROR: --srcs-file is required." >&2
usage
exit 2
fi
if [ ! -f "$SRCS_FILE" ]; then
echo "ERROR: --srcs-file '$SRCS_FILE' does not exist." >&2
exit 1
fi
if [ "$SOURCE_ONLY" -eq 1 ]; then
if [ -n "$CFLAGS_IN" ] || [ -n "$OPTIONS_H" ]; then
echo "ERROR: --source-only cannot be combined with --cflags/--options-h." >&2
exit 2
fi
else
if [ -n "$CFLAGS_IN" ] && [ -n "$OPTIONS_H" ]; then
echo "ERROR: pass only one of --cflags or --options-h." >&2
exit 2
fi
if [ -z "$CFLAGS_IN" ] && [ -z "$OPTIONS_H" ]; then
echo "ERROR: pass one of --cflags, --options-h, or --source-only." >&2
exit 2
fi
fi
if [ -z "$VERSION" ]; then
echo "ERROR: could not determine version; pass --version or check" >&2
echo " $ROOT/include/wolfboot/version.h" >&2
exit 1
fi
if [ ! -f "$GEN_SBOM" ]; then
echo "ERROR: gen-sbom not found at '$GEN_SBOM'." >&2
echo " Initialize the submodule: git submodule update --init lib/wolfssl" >&2
echo " or pass --gen-sbom /path/to/wolfssl/scripts/gen-sbom" >&2
exit 1
fi
# Default output names follow the component name so multiple artifacts
# (wolfboot, wolfboot-hal-<target>, wolfboot-zephyr, ...) don't collide.
[ -n "$CDX_OUT" ] || CDX_OUT="$NAME-$VERSION.cdx.json"
[ -n "$SPDX_OUT" ] || SPDX_OUT="$NAME-$VERSION.spdx.json"
# Temp files we own (cleaned up on exit). The trap captures and re-returns the
# real exit status so cleanup never masks it (a bare "[ -n x ] && rm" as the
# last statement would make a successful run exit non-zero).
_TMP_DH=""
_TMP_SF=""
_TMP_SCRUB=""
cleanup() {
_rc=$?
for _f in "$_TMP_DH" "$_TMP_SF" "$_TMP_SCRUB"; do
[ -n "$_f" ] && rm -f "$_f"
done
return $_rc
}
trap cleanup EXIT INT TERM HUP
# Redact absolute-path tokens from a flat #define header. A macro whose value
# is (or contains) an absolute path -- e.g. `#define PICO_SDK_PATH /home/x/sdk`
# from arch.mk's -DPICO_SDK_PATH=$(PICO_SDK_PATH) -- would otherwise make the
# SBOM machine-specific and leak the local file system. The redacted marker is
# constant, so the same configuration yields the same SBOM on every host.
# $1 = input header, $2 = output header.
scrub_defines() {
awk '
/^#define / {
name = $2
val = ""
for (i = 3; i <= NF; i++) val = (val == "" ? $i : val " " $i)
if (val == "") { print; next }
m = split(val, toks, " ")
nv = ""
for (i = 1; i <= m; i++) {
t = toks[i]
core = t
gsub(/"/, "", core)
# Absolute path: starts with "/" followed by at least one more char.
if (core ~ /^\/[^ ]+/) t = "<redacted-path>"
nv = (nv == "" ? t : nv " " t)
}
print "#define " name " " nv
next
}
{ print }
' "$1" > "$2"
}
# Optionally drop sources that do not exist on disk, matching the Make path's
# $(wildcard) behavior (e.g. keystore.c before it has been generated).
if [ "$SKIP_MISSING" -eq 1 ]; then
_TMP_SF=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-present.XXXXXX")
_dropped=0
while IFS= read -r _line || [ -n "$_line" ]; do
case "$_line" in
''|\#*) continue ;;
esac
if [ -f "$_line" ]; then
printf '%s\n' "$_line" >>"$_TMP_SF"
else
echo "WARNING: skipping missing source: $_line" >&2
_dropped=$((_dropped + 1))
fi
done < "$SRCS_FILE"
if [ ! -s "$_TMP_SF" ]; then
echo "ERROR: no existing source files remain after --skip-missing." >&2
exit 1
fi
[ "$_dropped" -gt 0 ] && echo " (--skip-missing dropped $_dropped file(s))" >&2
SRCS_FILE="$_TMP_SF"
fi
if [ "$SOURCE_ONLY" -eq 1 ]; then
# No build-config macros: hand gen-sbom an empty define header.
_TMP_DH=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-defines.XXXXXX")
DEFINES_H="$_TMP_DH"
elif [ -n "$OPTIONS_H" ]; then
if [ ! -f "$OPTIONS_H" ]; then
echo "ERROR: --options-h '$OPTIONS_H' does not exist." >&2
exit 1
fi
DEFINES_H="$OPTIONS_H"
else
# Extract -D tokens from CFLAGS and expand through the HOST compiler so the
# captured macro set reflects the effective configuration, independent of
# the (possibly cross) target toolchain.
_defs=""
for _t in $CFLAGS_IN; do
case "$_t" in
-D*) _defs="$_defs $_t" ;;
esac
done
_TMP_DH=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-defines.XXXXXX")
# shellcheck disable=SC2086
if ! $HOSTCC -dM -E -DWOLFSSL_USER_SETTINGS $_defs -x c /dev/null >"$_TMP_DH" 2>/dev/null; then
echo "ERROR: '$HOSTCC -dM -E' failed; install a host C compiler or set HOSTCC." >&2
exit 1
fi
DEFINES_H="$_TMP_DH"
fi
# Scrub absolute host paths out of the captured macros (unless disabled) so the
# SBOM is reproducible and does not leak the local file system. Applies to both
# the CFLAGS-derived header and a caller-supplied --options-h.
if [ "$SCRUB" -eq 1 ] && [ "$SOURCE_ONLY" -ne 1 ]; then
_TMP_SCRUB=$(mktemp "${TMPDIR:-/tmp}/wolfboot-sbom-scrub.XXXXXX")
scrub_defines "$DEFINES_H" "$_TMP_SCRUB"
DEFINES_H="$_TMP_SCRUB"
fi
echo "wolfBoot SBOM: name=$NAME version=$VERSION"
echo " sources: $SRCS_FILE"
echo " outputs: $CDX_OUT $SPDX_OUT"
"$PYTHON" "$GEN_SBOM" \
--name "$NAME" \
--version "$VERSION" \
--supplier "wolfSSL Inc." \
--license-file "$LICENSE_FILE" \
--options-h "$DEFINES_H" \
--srcs-file "$SRCS_FILE" \
--cdx-out "$CDX_OUT" \
--spdx-out "$SPDX_OUT"
echo "SBOM written: $CDX_OUT $SPDX_OUT"