feat(sbom): ship a CycloneDX bill of materials with every build
An air-gapped site cannot be scanned from anywhere else, so when a CVE lands the only way to answer 'is that component here, and at what version' was to RDP in and go looking. The frontend was the real blind spot: nothing recorded which version of leaflet, dompurify, jspdf or html2canvas ends up inside the compiled SPA. scripts/generate_sbom.py emits CycloneDX 1.6 covering both ecosystems - every pin in requirements.txt with the sha256 the installer enforces, and every package in package-lock.json. Build-only npm packages are marked scope 'excluded' rather than dropped, so 'not here' stays distinguishable from 'not looked for'. Dependency edges are real: uv's '# via' comments give the Python graph and package-lock gives the npm one. Hand-rolled rather than cyclonedx-py plus cyclonedx-npm because both inputs are already pinned and committed - this is a format translation, not a scan - and because the build box may be a work PC with nothing but Python and Node. It is deterministic by construction: same inputs, byte-identical output, so regenerating does not churn. Staged into the application tree by both builders, so it installs onto the server with the app. shopdb-admin.ps1 verify reports it and searches it by component name, which is the question actually being asked. Packages appearing at several depths in package-lock (node_modules/vite and node_modules/vitest/node_modules/vite) are merged, and a copy reachable outside the dev tree makes the component count as shipped. Emitting both produced duplicate bom-refs, which CycloneDX forbids and scanners reject; getting the dev merge backwards would have hidden a shipped package from a CVE search. Not covered by bundle-lock.json on purpose: its provenance is git, not the third-party payload.
This commit is contained in:
@@ -77,6 +77,15 @@ if [ -f "$REPO/deploy/windows/web.config" ]; then
|
||||
cp -a "$REPO/deploy/windows/web.config" "$OUT/deploy/windows/"
|
||||
fi
|
||||
|
||||
# A CycloneDX SBOM of everything this tree depends on, Python and npm together.
|
||||
# Staged INTO the tree so it installs onto the server with the application: an
|
||||
# air-gapped site cannot be scanned remotely, so the only way to answer "are we
|
||||
# exposed to this CVE, and where" is for the answer to be sitting on the box.
|
||||
# Generated from requirements.txt and package-lock.json, both already pinned and
|
||||
# committed, so it is a translation rather than a scan - and deterministic.
|
||||
echo "==> Generating SBOM ..."
|
||||
python3 "$REPO/scripts/generate_sbom.py" "$REPO" -o "$OUT/sbom.cdx.json"
|
||||
|
||||
# Stage the profile INTO the tree. This is what makes the set self-describing:
|
||||
# `flask plugin apply-profile` at provisioning time reads the same profile the
|
||||
# tree was staged from, so the installed plugin set and the shipped plugin code
|
||||
|
||||
Reference in New Issue
Block a user