Skip to content

Commit 04008be

Browse files
authored
Merge pull request #2 from sameehj/pr-10343-sbom
Pr 10343 SBOM
2 parents 30619d2 + be67594 commit 04008be

7 files changed

Lines changed: 1067 additions & 62 deletions

File tree

.github/workflows/sbom.yml

Lines changed: 336 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,336 @@
1+
name: SBOM Tests
2+
3+
# START OF COMMON SECTION
4+
on:
5+
push:
6+
branches: [ 'master', 'main', 'release/**' ]
7+
pull_request:
8+
branches: [ '*' ]
9+
10+
concurrency:
11+
group: ${{ github.workflow }}-${{ github.ref }}
12+
cancel-in-progress: true
13+
# END OF COMMON SECTION
14+
15+
jobs:
16+
# Tier 1 - pure-Python unit tests for scripts/gen-sbom.
17+
# No build, no autotools, no external deps. Runs in seconds and is the
18+
# cheapest gate for licence/UUID/timestamp logic regressions.
19+
unit:
20+
name: gen-sbom unit tests
21+
if: github.repository_owner == 'wolfssl'
22+
runs-on: ubuntu-24.04
23+
timeout-minutes: 5
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- name: Syntax check
28+
run: python3 -m py_compile scripts/gen-sbom
29+
30+
- name: Unit tests
31+
run: python3 -W error::ResourceWarning -m unittest scripts/test_gen_sbom.py -v
32+
33+
# Tier 2 - integration: build wolfSSL, generate the SBOMs, and assert
34+
# everything an external auditor or vulnerability scanner relies on.
35+
integration:
36+
name: SBOM integration (linux)
37+
if: github.repository_owner == 'wolfssl'
38+
runs-on: ubuntu-24.04
39+
needs: unit
40+
timeout-minutes: 20
41+
steps:
42+
- uses: actions/checkout@v4
43+
44+
# Pin tool versions; drift in any of these silently changes what
45+
# "valid" means and produces mystery CI failures.
46+
- name: Install SBOM validators
47+
run: |
48+
python3 -m pip install --user --upgrade pip
49+
python3 -m pip install --user \
50+
'spdx-tools==0.8.*' \
51+
'ntia-conformance-checker==5.*' \
52+
'cyclonedx-bom==7.*'
53+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
54+
55+
# Test fixture for the LicenseRef-+text matrix step. Using a fixture
56+
# rather than $PWD/COPYING decouples the test from upstream file
57+
# naming and makes the assertion exact ('FIXTURE LICENCE BODY').
58+
- name: Create license-text fixture
59+
run: echo 'FIXTURE LICENCE BODY' > /tmp/sbom-fixture-licence.txt
60+
61+
- name: Configure wolfSSL (shared + static)
62+
run: autoreconf -ivf && ./configure --enable-shared --enable-static
63+
64+
- name: Build + generate SBOM (default GPL)
65+
run: make sbom
66+
67+
# ---- Format-level validators -----------------------------------------
68+
69+
- name: SPDX 2.3 - NTIA Minimum Elements (2021)
70+
# Already validated structurally by pyspdxtools inside `make sbom`.
71+
# NTIA conformance is the additional contract auditors rely on.
72+
run: ntia-checker -c ntia wolfssl-*.spdx.json
73+
74+
- name: CycloneDX 1.6 - JSON schema validation
75+
run: |
76+
python3 - <<'PY'
77+
import glob, sys
78+
from cyclonedx.validation.json import JsonStrictValidator
79+
from cyclonedx.schema import SchemaVersion
80+
v = JsonStrictValidator(SchemaVersion.V1_6)
81+
for path in glob.glob('wolfssl-*.cdx.json'):
82+
with open(path) as f:
83+
errors = v.validate_str(f.read())
84+
if errors:
85+
print(f"INVALID: {path}: {errors}", file=sys.stderr)
86+
sys.exit(1)
87+
print(f"OK: {path}")
88+
PY
89+
90+
# ---- Artefact-integrity assertions ----------------------------------
91+
92+
- name: Library hash matches the SBOM
93+
# `make sbom` cleans its private staging tree on exit, so we install
94+
# to an independent prefix and re-hash the resulting library.
95+
# Search order matches gen-sbom's so we hash the same artefact.
96+
run: |
97+
rm -rf /tmp/_inst
98+
make install DESTDIR=/tmp/_inst >/dev/null
99+
LIB=""
100+
for cand in /tmp/_inst/usr/local/lib/libwolfssl.so.[0-9]* \
101+
/tmp/_inst/usr/local/lib/libwolfssl.so \
102+
/tmp/_inst/usr/local/lib/libwolfssl.a; do
103+
if [ -f "$cand" ]; then LIB="$cand"; break; fi
104+
done
105+
test -n "$LIB" || (echo "no installed library found"; exit 1)
106+
EXPECTED=$(python3 -c "
107+
import hashlib, sys
108+
h = hashlib.sha256()
109+
with open(sys.argv[1], 'rb') as f:
110+
for chunk in iter(lambda: f.read(65536), b''):
111+
h.update(chunk)
112+
print(h.hexdigest())" "$LIB")
113+
ACTUAL=$(python3 -c "
114+
import json, glob
115+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
116+
d = json.load(f)
117+
p = [x for x in d['packages'] if x['name'] == 'wolfssl'][0]
118+
print(p['checksums'][0]['checksumValue'])")
119+
test "$EXPECTED" = "$ACTUAL" || \
120+
{ echo "hash mismatch: expected=$EXPECTED actual=$ACTUAL"; exit 1; }
121+
122+
- name: CPE 2.3 and PURL identifiers well-formed
123+
# A typo in supplier or product name silently breaks every
124+
# downstream OSV / Trivy / Grype scan.
125+
run: |
126+
python3 - <<'PY'
127+
import glob, json, re, sys
128+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
129+
d = json.load(f)
130+
refs = {r['referenceType']: r['referenceLocator']
131+
for r in d['packages'][0]['externalRefs']}
132+
assert re.match(r'cpe:2\.3:a:wolfssl:wolfssl:[\d.]+:', refs['cpe23Type']), refs
133+
assert re.match(r'pkg:generic/wolfssl@[\d.]+', refs['purl']), refs
134+
print('identifiers ok:', refs)
135+
PY
136+
137+
# ---- Reproducibility -------------------------------------------------
138+
139+
- name: Reproducibility under SOURCE_DATE_EPOCH
140+
run: |
141+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
142+
SOURCE_DATE_EPOCH=1700000000 make sbom
143+
sha256sum wolfssl-*.cdx.json wolfssl-*.spdx.json > /tmp/a.sums
144+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
145+
SOURCE_DATE_EPOCH=1700000000 make sbom
146+
sha256sum wolfssl-*.cdx.json wolfssl-*.spdx.json > /tmp/b.sums
147+
diff /tmp/a.sums /tmp/b.sums
148+
149+
# ---- Licence-override matrix ----------------------------------------
150+
151+
- name: License matrix - default GPL
152+
# Detected from LICENSING. The current upstream file reads
153+
# "GNU General Public License version 3" without "or later", so
154+
# detect_license returns GPL-3.0-only. If LICENSING is updated to
155+
# add "or any later version", switch this assertion to
156+
# GPL-3.0-or-later.
157+
run: |
158+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
159+
make sbom
160+
python3 - <<'PY'
161+
import glob, json
162+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
163+
d = json.load(f)
164+
assert d['packages'][0]['licenseConcluded'].startswith('GPL-3.0-'), \
165+
d['packages'][0]['licenseConcluded']
166+
assert 'hasExtractedLicensingInfos' not in d
167+
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
168+
cdx = json.load(f)
169+
lic = cdx['metadata']['component']['licenses']
170+
assert lic == [{'license': {'id': d['packages'][0]['licenseConcluded']}}], lic
171+
print('default GPL: ok ->', lic)
172+
PY
173+
174+
- name: License matrix - LicenseRef + text
175+
run: |
176+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
177+
make sbom \
178+
SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial \
179+
SBOM_LICENSE_TEXT=/tmp/sbom-fixture-licence.txt
180+
python3 - <<'PY'
181+
import glob, json
182+
with open('/tmp/sbom-fixture-licence.txt') as f:
183+
expected = f.read()
184+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
185+
d = json.load(f)
186+
infos = d['hasExtractedLicensingInfos']
187+
assert len(infos) == 1
188+
assert infos[0]['licenseId'] == 'LicenseRef-wolfSSL-Commercial'
189+
assert infos[0]['extractedText'] == expected
190+
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
191+
cdx = json.load(f)
192+
lic = cdx['metadata']['component']['licenses'][0]['license']
193+
assert lic['name'] == 'LicenseRef-wolfSSL-Commercial'
194+
assert lic['text']['content'] == expected
195+
print('LicenseRef + text: ok')
196+
PY
197+
# The output of this run must still pass NTIA and CDX validators.
198+
ntia-checker -c ntia wolfssl-*.spdx.json
199+
python3 - <<'PY'
200+
import glob, sys
201+
from cyclonedx.validation.json import JsonStrictValidator
202+
from cyclonedx.schema import SchemaVersion
203+
v = JsonStrictValidator(SchemaVersion.V1_6)
204+
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
205+
errs = v.validate_str(f.read())
206+
sys.exit(1 if errs else 0)
207+
PY
208+
209+
- name: License matrix - LicenseRef without text must FAIL
210+
# gen-sbom must refuse to emit a SBOM that names a LicenseRef-*
211+
# but doesn't embed its text - that combo is invalid per SPDX 2.3
212+
# and any "successfully generated" output would mislead auditors.
213+
run: |
214+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
215+
if make sbom SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial \
216+
2>/tmp/err; then
217+
echo "FAIL: gen-sbom should have refused this configuration"
218+
exit 1
219+
fi
220+
grep -q 'license-text was not provided' /tmp/err || \
221+
{ echo "FAIL: error message missing actionable hint"; \
222+
cat /tmp/err; exit 1; }
223+
if ls wolfssl-*.spdx.json >/dev/null 2>&1; then
224+
echo "FAIL: SBOM file should not exist after refusal"
225+
exit 1
226+
fi
227+
228+
- name: License matrix - compound expression
229+
run: |
230+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
231+
make sbom \
232+
SBOM_LICENSE_OVERRIDE='GPL-3.0-only OR LicenseRef-wolfSSL-Commercial' \
233+
SBOM_LICENSE_TEXT=/tmp/sbom-fixture-licence.txt
234+
python3 - <<'PY'
235+
import glob, json
236+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
237+
d = json.load(f)
238+
assert len(d['hasExtractedLicensingInfos']) == 1
239+
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
240+
cdx = json.load(f)
241+
entry = cdx['metadata']['component']['licenses'][0]
242+
assert 'expression' in entry, entry
243+
print('compound expression: ok')
244+
PY
245+
246+
- name: License matrix - simple SPDX override
247+
run: |
248+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
249+
make sbom SBOM_LICENSE_OVERRIDE=Apache-2.0
250+
python3 - <<'PY'
251+
import glob, json
252+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
253+
d = json.load(f)
254+
assert 'hasExtractedLicensingInfos' not in d
255+
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
256+
cdx = json.load(f)
257+
lic = cdx['metadata']['component']['licenses'][0]['license']
258+
assert lic == {'id': 'Apache-2.0'}, lic
259+
print('simple SPDX override: ok')
260+
PY
261+
262+
# ---- Distribution + install hooks -----------------------------------
263+
264+
- name: Tarball roundtrip (make dist -> ./configure -> make sbom)
265+
# If a future change adds a new helper file but forgets EXTRA_DIST,
266+
# the tarball will not contain it and this step fails.
267+
run: |
268+
rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
269+
make dist
270+
mkdir /tmp/tb
271+
tar -xzf wolfssl-*.tar.gz -C /tmp/tb
272+
cd /tmp/tb/wolfssl-*
273+
./configure --enable-shared
274+
make sbom
275+
276+
- name: Install-sbom / uninstall hook
277+
# `install-sbom` is a separate target (intentional - SBOM generation
278+
# has heavy deps like pyspdxtools that we do not want firing on
279+
# every `make install`). `make uninstall` runs uninstall-hook,
280+
# which removes both regular and SBOM artefacts idempotently.
281+
run: |
282+
rm -rf /tmp/_inst2
283+
make install DESTDIR=/tmp/_inst2 >/dev/null
284+
make install-sbom DESTDIR=/tmp/_inst2
285+
ls /tmp/_inst2/usr/local/share/doc/wolfssl/wolfssl-*.spdx.json \
286+
/tmp/_inst2/usr/local/share/doc/wolfssl/wolfssl-*.cdx.json \
287+
/tmp/_inst2/usr/local/share/doc/wolfssl/wolfssl-*.spdx
288+
make uninstall DESTDIR=/tmp/_inst2
289+
if ls /tmp/_inst2/usr/local/share/doc/wolfssl/wolfssl-*.spdx.json \
290+
2>/dev/null; then
291+
echo "uninstall-hook did not remove SBOM artefacts"
292+
exit 1
293+
fi
294+
295+
# Tier 2 (macOS) - smoke test that gen-sbom finds .dylib artefacts and
296+
# that the autotools target works on Mach-O. Linux already exercises
297+
# the heavy validation matrix; this job is intentionally minimal so the
298+
# macOS runner minutes go to portability coverage, not duplicated checks.
299+
integration-macos:
300+
name: SBOM integration (macos)
301+
if: github.repository_owner == 'wolfssl'
302+
runs-on: macos-latest
303+
needs: unit
304+
timeout-minutes: 20
305+
steps:
306+
- uses: actions/checkout@v4
307+
308+
- name: Install build deps and SBOM validators
309+
run: |
310+
brew install autoconf automake libtool
311+
python3 -m pip install --user --break-system-packages \
312+
'spdx-tools==0.8.*'
313+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
314+
# On some macOS runners pyspdxtools lands in
315+
# Library/Python/<ver>/bin; symlink to a known-on-PATH location.
316+
for d in "$HOME/Library/Python"/*/bin; do
317+
[ -x "$d/pyspdxtools" ] && \
318+
echo "$d" >> "$GITHUB_PATH"
319+
done
320+
321+
- name: Configure wolfSSL (shared)
322+
run: autoreconf -ivf && ./configure --enable-shared
323+
324+
- name: Build + generate SBOM (verifies .dylib detection)
325+
run: make sbom
326+
327+
- name: SBOM hashed a real .dylib
328+
run: |
329+
python3 - <<'PY'
330+
import glob, json, re
331+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
332+
d = json.load(f)
333+
checksum = d['packages'][0]['checksums'][0]['checksumValue']
334+
assert re.fullmatch(r'[0-9a-f]{64}', checksum), checksum
335+
print('macOS SBOM checksum well-formed:', checksum)
336+
PY

0 commit comments

Comments
 (0)