mirror of https://github.com/wolfSSL/wolfBoot.git
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
parent
6ba2172695
commit
142de77ed3
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
81
Makefile
81
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
|
||||
|
|
|
|||
22
README.md
22
README.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)"
|
||||
)
|
||||
|
|
@ -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=<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 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 <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 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-<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.
|
||||
|
|
@ -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()
|
||||
|
|
@ -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()
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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)
|
||||
|
|
@ -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()
|
||||
|
|
@ -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"
|
||||
Loading…
Reference in New Issue