1+ #! /usr/bin/env bash
2+ #
3+ # generate-sbom.sh - Generate and validate CycloneDX SBOM
4+ #
5+ # Usage: ./generate-sbom.sh [output-file]
6+ #
7+ # Environment variables:
8+ # GITHUB_OUTPUT - Path to GitHub Actions output file (optional)
9+ #
10+
11+ set -euo pipefail
12+
13+ SBOM_FILE=" ${1:- sbom.json} "
14+ TEMP_SBOM=" sbom-new.json"
15+ CYCLONEDX_CLI=" /tmp/cyclonedx"
16+ CYCLONEDX_CLI_URL=" https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.29.1/cyclonedx-linux-x64"
17+ JQ_NORMALIZER=' del(.serialNumber) | del(.metadata.timestamp) | walk(if type == "object" and .timestamp then .timestamp = "TIMESTAMP_NORMALIZED" else . end)'
18+
19+ echo " Starting SBOM generation (output: $SBOM_FILE )"
20+
21+ echo " Generating SBOM for 'node' project..."
22+
23+ if ! npx @cyclonedx/cyclonedx-npm \
24+ --omit dev \
25+ --package-lock-only \
26+ --output-file " $TEMP_SBOM " \
27+ --output-format json \
28+ --spec-version 1.5; then
29+ echo " ERROR: Failed to generate SBOM" >&2
30+ exit 1
31+ fi
32+
33+ if [[ ! -f " $TEMP_SBOM " ]]; then
34+ echo " ERROR: SBOM file not found after generation" >&2
35+ exit 1
36+ fi
37+
38+ echo " SBOM file generated: $TEMP_SBOM "
39+
40+ echo " Downloading CycloneDX CLI..."
41+
42+ if ! curl -L -s -o " $CYCLONEDX_CLI " " $CYCLONEDX_CLI_URL " ; then
43+ echo " ERROR: Failed to download CycloneDX CLI" >&2
44+ exit 1
45+ fi
46+
47+ chmod +x " $CYCLONEDX_CLI "
48+
49+ if [[ ! -x " $CYCLONEDX_CLI " ]]; then
50+ echo " ERROR: CycloneDX CLI is not executable" >&2
51+ exit 1
52+ fi
53+
54+ echo " CycloneDX CLI ready at $CYCLONEDX_CLI "
55+
56+ echo " Validating SBOM: $TEMP_SBOM "
57+
58+ if ! " $CYCLONEDX_CLI " validate --input-file " $TEMP_SBOM " --fail-on-errors; then
59+ echo " ERROR: SBOM validation failed for $TEMP_SBOM " >&2
60+ exit 1
61+ fi
62+
63+ echo " SBOM validation passed: $TEMP_SBOM "
64+
65+ echo " Checking for SBOM changes..."
66+
67+ HAS_CHANGES=" false"
68+
69+ if [[ ! -f " $SBOM_FILE " ]]; then
70+ echo " No existing $SBOM_FILE found, creating initial version"
71+ mv " $TEMP_SBOM " " $SBOM_FILE "
72+ HAS_CHANGES=" true"
73+ else
74+ echo " Comparing new SBOM with existing $SBOM_FILE ..."
75+
76+ # Try cyclonedx diff for component-level comparison
77+ DIFF_OUTPUT=$( " $CYCLONEDX_CLI " diff " $SBOM_FILE " " $TEMP_SBOM " --component-versions 2> /dev/null || true)
78+
79+ if echo " $DIFF_OUTPUT " | grep -q " ^None$" ; then
80+ echo " No component changes detected via cyclonedx diff"
81+
82+ # Double-check with jq normalization (excludes metadata like timestamps)
83+ if diff -q \
84+ <( jq -r " $JQ_NORMALIZER " < " $SBOM_FILE " ) \
85+ <( jq -r " $JQ_NORMALIZER " < " $TEMP_SBOM " ) > /dev/null 2>&1 ; then
86+ echo " No meaningful changes detected in SBOM"
87+ rm -f " $TEMP_SBOM "
88+ HAS_CHANGES=" false"
89+ else
90+ echo " Changes detected in SBOM (non-component changes)"
91+ mv " $TEMP_SBOM " " $SBOM_FILE "
92+ HAS_CHANGES=" true"
93+ fi
94+ else
95+ echo " Component changes detected:"
96+ echo " $DIFF_OUTPUT "
97+ mv " $TEMP_SBOM " " $SBOM_FILE "
98+ HAS_CHANGES=" true"
99+ fi
100+ fi
101+
102+ if [[ -n " ${GITHUB_OUTPUT:- } " ]]; then
103+ echo " HAS_CHANGES=${HAS_CHANGES} " >> " $GITHUB_OUTPUT "
104+ fi
105+ echo " Output: HAS_CHANGES=${HAS_CHANGES} "
106+
107+ if [[ ! -f " $SBOM_FILE " ]]; then
108+ echo " ERROR: Final SBOM file not found at $SBOM_FILE " >&2
109+ exit 1
110+ fi
111+
112+ echo " SBOM file validated: $SBOM_FILE "
113+ echo " SBOM generation completed successfully"
0 commit comments