wolfssl-examples/cra-kit/scripts/generate-wolfssh-sbom.sh

371 lines
16 KiB
Bash
Executable File

#!/bin/sh
# Generate wolfSSH component SBOM (autotools make sbom, or embedded gen-sbom).
#
# Mode selection:
# CRA_SBOM_MODE=autotools|embedded (default: autotools)
# autotools: builds libwolfssh.so and runs `make sbom`
# embedded: hashes wolfSSH source files directly (no .so is produced when
# wolfSSH is compiled into firmware), via wolfSSL's gen-sbom.
#
# Required variables:
# WOLFSSL_DIR=path/to/wolfssl (source tree root; provides gen-sbom)
# WOLFSSH_DIR=path/to/wolfssh (source tree root)
#
# Embedded-mode variables:
# CRA_SBOM_BUILD_DIR=path/to/build (embedded: dir containing
# compile_commands.json for CMake / ESP-IDF
# / Zephyr builds, used to extract the exact
# wolfSSH .c files on the link line)
# CRA_SBOM_SRCS_FILE=path/to/srcs.txt (embedded: explicit list of wolfSSH .c
# paths, one per line; takes priority over
# every other source-resolution method)
# CRA_SBOM_KEIL_PROJECT=path (embedded: auto-extract from Keil .uvprojx)
# CRA_SBOM_IAR_PROJECT=path (embedded: auto-extract from IAR .ewp)
# CRA_SBOM_MAKEFILE_DIR=path (embedded: auto-extract via make -n dry-run)
# CRA_SBOM_NO_HASH=true (embedded: emit SBOM without an artifact
# hash, skipping the source list — for NDA
# customers who cannot share source lists;
# WARNING: not suitable for production compliance)
#
# Optional variables:
# CRA_SBOM_OUT_DIR=<path> (output directory; default auditor-packet)
# CRA_LICENSE_OVERRIDE=<SPDX-id> (e.g. LicenseRef-wolfSSH-Commercial)
# CRA_LICENSE_TEXT=<path> (required when CRA_LICENSE_OVERRIDE is a
# LicenseRef-* id: plain-text licence embedded
# in the SBOM; make sbom / gen-sbom hard-fail
# without it.)
# POSIX sh (script is #!/bin/sh and run via `sh`); dash has no `set -o pipefail`,
# so we use `set -eu` like the rest of the kit. Pipelines that must not mask a
# failed first stage are checked explicitly instead.
set -eu
# Accumulator for temp files (embedded source extraction); cleaned up on exit.
# Why: mktemp temp files must not leak if any later command fails under set -e;
# a single EXIT trap removes them on every exit path including errors.
_auto_tempfiles=""
_cra_auto_tempfiles=""
# _cra_auto_tempfiles is populated by _cra-sbom-extract.sh's helpers; clean both.
trap 'rm -f ${_auto_tempfiles:-} ${_cra_auto_tempfiles:-}' EXIT
SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
KIT_DIR=$(dirname "$SCRIPT_DIR")
# shared extraction methods (Keil, IAR, Makefile, compile_commands.json)
# shellcheck disable=SC1091 # sourced helper, resolved at runtime
. "$SCRIPT_DIR/_cra-sbom-extract.sh"
# shellcheck disable=SC2015 # `|| true` is a deliberate set -e guard, not if-then-else
# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional
WOLFSSL_DIR=${WOLFSSL_DIR:-$(cd "$KIT_DIR/../../wolfssl" 2>/dev/null && pwd || true)}
# shellcheck disable=SC2015 # fallback to unset on cd failure is intentional
WOLFSSH_DIR=${WOLFSSH_DIR:-$(cd "$KIT_DIR/../../wolfSSH" 2>/dev/null && pwd || true)}
OUT_DIR=${CRA_SBOM_OUT_DIR:-"$KIT_DIR/auditor-packet/wolfssh-component"}
if [ -z "${WOLFSSL_DIR:-}" ] || [ ! -d "$WOLFSSL_DIR" ]; then
echo "ERROR: wolfSSL source not found." >&2
echo " Set WOLFSSL_DIR to your wolfssl checkout (sibling of wolfssl-examples)." >&2
exit 1
fi
if [ -z "${WOLFSSH_DIR:-}" ] || [ ! -d "$WOLFSSH_DIR" ]; then
echo "ERROR: wolfSSH source not found." >&2
echo " Set WOLFSSH_DIR to your wolfSSH checkout (sibling of wolfssl-examples)." >&2
exit 1
fi
VERSION=$(sed -n \
's/.*LIBWOLFSSH_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
"$WOLFSSH_DIR/wolfssh/version.h" 2>/dev/null || true)
if [ -z "$VERSION" ]; then
echo "ERROR: could not extract LIBWOLFSSH_VERSION_STRING from $WOLFSSH_DIR/wolfssh/version.h" >&2
exit 1
fi
mkdir -p "$OUT_DIR"
CDX_OUT="$OUT_DIR/wolfssh-${VERSION}.cdx.json"
SPDX_OUT="$OUT_DIR/wolfssh-${VERSION}.spdx.json"
echo "wolfSSL tree: $WOLFSSL_DIR"
echo "wolfSSH tree: $WOLFSSH_DIR"
echo "Outputs: $CDX_OUT"
echo " $SPDX_OUT"
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
echo "License override: $CRA_LICENSE_OVERRIDE"
fi
# A LicenseRef-* override (e.g. the commercial license) requires the actual
# licence text to be embedded in the SBOM (SPDX 2.3 §10.1). Both gen-sbom and
# `make sbom` hard-fail without it, so catch the omission here with an
# actionable message instead of letting the run die deep in the generator.
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
case "$CRA_LICENSE_OVERRIDE" in
LicenseRef-*)
if [ -z "${CRA_LICENSE_TEXT:-}" ]; then
echo "ERROR: CRA_LICENSE_OVERRIDE=$CRA_LICENSE_OVERRIDE is a LicenseRef-* identifier," >&2
echo " but CRA_LICENSE_TEXT is not set. SPDX 2.3 requires the licence text to be" >&2
echo " embedded for any LicenseRef-* used in licenseConcluded/licenseDeclared." >&2
echo " Re-run with CRA_LICENSE_TEXT=/path/to/wolfssh-commercial-license.txt." >&2
exit 1
fi
if [ ! -f "$CRA_LICENSE_TEXT" ]; then
echo "ERROR: CRA_LICENSE_TEXT=$CRA_LICENSE_TEXT not found." >&2
exit 1
fi
;;
esac
fi
# Canonicalize CRA_LICENSE_TEXT to an absolute path: make sbom runs inside a
# `cd "$WOLFSSH_DIR"` subshell, where a relative path would resolve against the
# wolfSSH tree rather than the caller's CWD.
if [ -n "${CRA_LICENSE_TEXT:-}" ] && [ -f "$CRA_LICENSE_TEXT" ]; then
CRA_LICENSE_TEXT=$(CDPATH='' cd -- "$(dirname -- "$CRA_LICENSE_TEXT")" && pwd)/$(basename -- "$CRA_LICENSE_TEXT")
echo "License text: $CRA_LICENSE_TEXT"
fi
# Pick a Python that can `import pcpp` (pip may target a different python3 than
# the one first on PATH). pcpp lets gen-sbom walk settings.h without a compiler.
_python_with_pcpp() {
for py in ${CRA_PYTHON:-} python3 python; do
[ -n "$py" ] || continue
if command -v "$py" >/dev/null 2>&1 && \
"$py" -c "import pcpp" 2>/dev/null; then
echo "$py"
return 0
fi
done
return 1
}
# Resolve the wolfSSH source list for embedded mode into the file named by $1.
#
# Source set rule: ONLY wolfSSH protocol sources (${WOLFSSH_DIR}/src/*.c) are
# product-owned and belong in this component. The wolfcrypt / wolfSSL sources
# that wolfSSH links against are NOT included here — they are a separate
# component covered by generate-wolfssl-sbom.sh and referenced as a dependency.
# Mixing them in would double-count the crypto library across two SBOMs.
#
# Priority order (first match wins), all but the last handled by
# _cra_extract_srcs from _cra-sbom-extract.sh:
# 1. CRA_SBOM_SRCS_FILE — explicit user list beats anything inferred, because
# the user knows their exact link line.
# 2. CRA_SBOM_KEIL_PROJECT — parse Keil .uvprojx, filter to WOLFSSH_DIR.
# 3. CRA_SBOM_IAR_PROJECT — parse IAR .ewp, filter to WOLFSSH_DIR.
# 4. CRA_SBOM_MAKEFILE_DIR — `make -n` dry-run, filter to WOLFSSH_DIR.
# 5. compile_commands.json at CRA_SBOM_BUILD_DIR — per-build extraction for
# CMake / ESP-IDF / Zephyr.
# 6. Default: all ${WOLFSSH_DIR}/src/*.c sorted — the "all sources" fallback
# for toolchains where we cannot infer the exact
# subset compiled. Listing all sources is the safe
# over-approximation: it never omits a file that
# shipped.
_resolve_wolfssh_srcs() {
_out="$1"
# Try every env-var-driven extraction method (SRCS_FILE, Keil, IAR,
# Makefile, compile_commands.json). rc=2 means none was set: fall back to
# the wolfSSH default glob below.
_cra_rc=0
_cra_extract_srcs "$WOLFSSH_DIR" "wolfssh" "$_out" || _cra_rc=$?
if [ "$_cra_rc" -eq 0 ]; then
return 0
elif [ "$_cra_rc" -ne 2 ]; then
exit 1 # _cra_extract_srcs already printed the error
fi
# No extraction env var set: every wolfSSH src/*.c. A POSIX glob expands in
# sorted order; guard the no-match case where the pattern stays literal.
: > "$_out"
for _c in "$WOLFSSH_DIR"/src/*.c; do
[ -f "$_c" ] || continue
printf '%s\n' "$_c" >> "$_out"
done
if [ ! -s "$_out" ]; then
echo "ERROR: no wolfSSH sources found in $WOLFSSH_DIR/src/*.c." >&2
exit 1
fi
_n=$(wc -l < "$_out" | tr -d ' ')
echo " Using all $_n wolfSSH sources from $WOLFSSH_DIR/src/*.c (default)"
return 0
}
_run_embedded() {
echo "==> Embedded path: gen-sbom hashing wolfSSH source files"
GEN="$WOLFSSL_DIR/scripts/gen-sbom"
if [ ! -f "$GEN" ]; then
echo "ERROR: $GEN not found (need a wolfSSL tree with SBOM support)." >&2
exit 1
fi
# gen-sbom's embedded entry point walks wolfSSL's settings.h to resolve the
# build config. wolfSSH headers pull in wolfSSL headers, so we reuse the same
# settings.h + kit user_settings.h the wolfssl embedded path uses.
SETTINGS_H="$WOLFSSL_DIR/wolfssl/wolfcrypt/settings.h"
if [ ! -f "$SETTINGS_H" ]; then
echo "ERROR: $SETTINGS_H not found." >&2
exit 1
fi
if [ ! -f "$KIT_DIR/user_settings.h" ]; then
echo "ERROR: $KIT_DIR/user_settings.h missing (demo WOLFSSL_USER_SETTINGS)." >&2
exit 1
fi
if ! command -v python3 >/dev/null 2>&1; then
echo "ERROR: python3 not found in PATH (required by gen-sbom)." >&2
exit 1
fi
# CRA_SBOM_NO_HASH emits a placeholder checksum and skips the source list
# entirely (for NDA customers who cannot share source lists).
if [ "${CRA_SBOM_NO_HASH:-}" = "true" ] || [ "${CRA_SBOM_NO_HASH:-}" = "1" ]; then
echo " NOTE: CRA_SBOM_NO_HASH=true: emitting SBOM without artifact hash."
echo " WARNING: not suitable for production CRA compliance." >&2
set -- --no-artifact-hash --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT"
else
# Resolve the source list into a temp file (cleaned up by the EXIT trap).
_srcs=$(mktemp "${TMPDIR:-/tmp}/wolfssh-srcs.XXXXXX") || {
echo "ERROR: mktemp failed for the source-list temp file." >&2
exit 1
}
_auto_tempfiles="${_auto_tempfiles:-} $_srcs"
_resolve_wolfssh_srcs "$_srcs"
# Validate every resolved path exists before handing the file to gen-sbom.
while IFS= read -r _src; do
[ -n "$_src" ] || continue
if [ ! -f "$_src" ]; then
echo "ERROR: source file does not exist: $_src" >&2
exit 1
fi
done < "$_srcs"
if [ ! -s "$_srcs" ]; then
echo "ERROR: resolved source list is empty." >&2
exit 1
fi
set -- --srcs-file "$_srcs" --cdx-out "$CDX_OUT" --spdx-out "$SPDX_OUT"
fi
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
set -- "$@" --license-override "$CRA_LICENSE_OVERRIDE"
if [ -n "${CRA_LICENSE_TEXT:-}" ]; then
set -- "$@" --license-text "$CRA_LICENSE_TEXT"
fi
fi
if _py=$(_python_with_pcpp); then
echo " Using $_py (pcpp) for --user-settings"
else
echo "ERROR: no python3/python with pcpp installed (required for embedded mode)." >&2
echo " Install it on the same interpreter: python3 -m pip install pcpp" >&2
exit 1
fi
"$_py" "$GEN" \
--name wolfssh --version "$VERSION" \
--license-file "$WOLFSSH_DIR/LICENSING" \
--user-settings "$SETTINGS_H" \
--user-settings-include "$WOLFSSL_DIR" \
--user-settings-include "$KIT_DIR" \
--user-settings-define WOLFSSL_USER_SETTINGS \
"$@"
# Verify gen-sbom actually produced both outputs and they are non-empty.
for _f in "$CDX_OUT" "$SPDX_OUT"; do
if [ ! -s "$_f" ]; then
echo "ERROR: expected output missing or empty: $_f" >&2
exit 1
fi
done
}
_run_autotools() {
echo "==> Autotools path: make sbom"
# `make sbom` names its output after the wolfSSH TREE's version
# (PACKAGE_VERSION). Detect mismatches early so the cp below doesn't fail
# with a cryptic "No such file or directory" under `set -eu`.
_tree_ver=$(sed -n \
's/.*LIBWOLFSSH_VERSION_STRING[[:space:]]*"\([^"]*\)".*/\1/p' \
"$WOLFSSH_DIR/wolfssh/version.h" 2>/dev/null || true)
if [ -n "$_tree_ver" ] && [ "$_tree_ver" != "$VERSION" ]; then
echo "ERROR: wolfSSH tree is version $_tree_ver but expected $VERSION." >&2
exit 1
fi
(cd "$WOLFSSH_DIR" && {
if [ ! -f Makefile ]; then
echo " Running ./configure first..."
./configure --with-wolfssl="$WOLFSSL_DIR"
fi
if [ -n "${CRA_LICENSE_OVERRIDE:-}" ]; then
if [ -n "${CRA_LICENSE_TEXT:-}" ]; then
make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \
SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE" \
SBOM_LICENSE_TEXT="$CRA_LICENSE_TEXT"
else
make sbom WOLFSSL_DIR="$WOLFSSL_DIR" \
SBOM_LICENSE_OVERRIDE="$CRA_LICENSE_OVERRIDE"
fi
else
make sbom WOLFSSL_DIR="$WOLFSSL_DIR"
fi
cp -f "wolfssh-${VERSION}.cdx.json" "$CDX_OUT"
cp -f "wolfssh-${VERSION}.spdx.json" "$SPDX_OUT"
if [ -f "wolfssh-${VERSION}.spdx" ]; then
cp -f "wolfssh-${VERSION}.spdx" "$OUT_DIR/"
fi
})
}
MODE=${CRA_SBOM_MODE:-autotools}
case "$MODE" in
embedded) _run_embedded ;;
autotools) _run_autotools ;;
*)
echo "ERROR: CRA_SBOM_MODE must be 'autotools' or 'embedded', not '$MODE'" >&2
exit 1
;;
esac
# ---- Post-process: defensive PURL canonicalization ----
# Current gen-sbom already emits pkg:github/wolfSSL/wolfSSH@vX natively for
# known component names; the rewrite below is a defensive no-op kept only for
# older generator versions that may emit pkg:generic/wolfssh@X.
if ! CDX_OUT="$CDX_OUT" SPDX_OUT="$SPDX_OUT" \
python3 <<'PY'
import json, os, pathlib
cdx = pathlib.Path(os.environ["CDX_OUT"])
spdx = pathlib.Path(os.environ["SPDX_OUT"])
GENERIC = "pkg:generic/wolfssh@"
GITHUB = "pkg:github/wolfSSL/wolfSSH@v"
def canonicalize_purl(s):
if isinstance(s, str) and s.startswith(GENERIC):
return GITHUB + s[len(GENERIC):]
return s
if cdx.exists():
d = json.loads(cdx.read_text())
comp = d.get("metadata", {}).get("component", {})
comp["purl"] = canonicalize_purl(comp.get("purl", ""))
cdx.write_text(json.dumps(d, indent=2) + "\n")
print(f"Post-processed {cdx.name}")
if spdx.exists():
d = json.loads(spdx.read_text())
for pkg in d.get("packages", []):
for ref in pkg.get("externalRefs", []):
if ref.get("referenceType") == "purl":
ref["referenceLocator"] = canonicalize_purl(ref.get("referenceLocator", ""))
spdx.write_text(json.dumps(d, indent=2) + "\n")
print(f"Post-processed {spdx.name}")
PY
then
echo "ERROR: post-process failed (PURL canonicalization incomplete)." >&2
echo " The emitted SBOM may carry pkg:generic PURLs; not trusting it." >&2
exit 1
fi
echo "Done. SBOM outputs:"
echo " $CDX_OUT"
echo " $SPDX_OUT"