diff --git a/.github/workflows/test-sbom.yml b/.github/workflows/test-sbom.yml new file mode 100644 index 00000000..378ff6e2 --- /dev/null +++ b/.github/workflows/test-sbom.yml @@ -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 diff --git a/.gitignore b/.gitignore index e8578086..f600e316 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index e467ae9f..9c753d0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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") diff --git a/Makefile b/Makefile index e69fec60..b0ca1a1d 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index e3fed934..84938197 100644 --- a/README.md +++ b/README.md @@ -142,15 +142,29 @@ make sbom TARGET= SIGN= HASH= `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= SIGN=` | +| CMake (and Pico SDK) | `cmake --build --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 ...` | +| Any IDE with a compilation database | `tools/scripts/ide-sbom/compdb_sbom.py compile_commands.json` | +| Per-HAL component | `make sbom-hal TARGET=` | +| Zephyr TEE/PSA module | `tools/scripts/ide-sbom/zephyr_sbom.py` | Output files are written to the build directory as `wolfboot-.cdx.json` (CycloneDX 1.6) and `wolfboot-.spdx.json` (SPDX 2.3 JSON), where `` 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 diff --git a/cmake/sbom.cmake b/cmake/sbom.cmake new file mode 100644 index 00000000..a309edd9 --- /dev/null +++ b/cmake/sbom.cmake @@ -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 --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. $) 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)" +) diff --git a/docs/SBOM.md b/docs/SBOM.md new file mode 100644 index 00000000..965b4db5 --- /dev/null +++ b/docs/SBOM.md @@ -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 1–4) + +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= SIGN= HASH= +``` + +`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 5–6) + +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 /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 ` (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 8–11) + +### 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-`, so nothing collides. + +### Per-HAL SBOM + +The hardware abstraction layer for a target (`hal/hal.c`, `hal/.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= SIGN= +``` + +This reuses the real build `CFLAGS`, so the captured configuration matches the +bootloader build. Output: `wolfboot-hal--.{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-.{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-.cdx.json` — CycloneDX 1.6 +* `wolfboot-.spdx.json` — SPDX 2.3 + +`` 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. diff --git a/tools/scripts/ide-sbom/compdb_sbom.py b/tools/scripts/ide-sbom/compdb_sbom.py new file mode 100755 index 00000000..ea9c30c7 --- /dev/null +++ b/tools/scripts/ide-sbom/compdb_sbom.py @@ -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() diff --git a/tools/scripts/ide-sbom/iar_sbom.py b/tools/scripts/ide-sbom/iar_sbom.py new file mode 100755 index 00000000..e3bc775d --- /dev/null +++ b/tools/scripts/ide-sbom/iar_sbom.py @@ -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 entries) + * the compiled source files ( ...), 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 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() diff --git a/tools/scripts/ide-sbom/route_through_sbom.sh b/tools/scripts/ide-sbom/route_through_sbom.sh new file mode 100755 index 00000000..dced3bb9 --- /dev/null +++ b/tools/scripts/ide-sbom/route_through_sbom.sh @@ -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/.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 (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 "$@" diff --git a/tools/scripts/ide-sbom/validate_sbom.py b/tools/scripts/ide-sbom/validate_sbom.py new file mode 100755 index 00000000..a39e9ec2 --- /dev/null +++ b/tools/scripts/ide-sbom/validate_sbom.py @@ -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-, 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) diff --git a/tools/scripts/ide-sbom/zephyr_sbom.py b/tools/scripts/ide-sbom/zephyr_sbom.py new file mode 100755 index 00000000..f16d7a88 --- /dev/null +++ b/tools/scripts/ide-sbom/zephyr_sbom.py @@ -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: /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-.{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() diff --git a/tools/scripts/wolfboot-sbom.sh b/tools/scripts/wolfboot-sbom.sh new file mode 100755 index 00000000..2dbbed7d --- /dev/null +++ b/tools/scripts/wolfboot-sbom.sh @@ -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: /LICENSE). + --cdx-out PATH CycloneDX output (default: -.cdx.json). + --spdx-out PATH SPDX output (default: -.spdx.json). + --gen-sbom PATH Path to wolfSSL scripts/gen-sbom + (default: /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-, 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 = "" + 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"