3333 # Tier 2 - integration: build wolfSSL, generate the SBOMs, and assert
3434 # everything an external auditor or vulnerability scanner relies on.
3535 integration :
36- name : SBOM integration
36+ name : SBOM integration (linux)
3737 if : github.repository_owner == 'wolfssl'
3838 runs-on : ubuntu-24.04
3939 needs : unit
5252 'cyclonedx-bom==7.*'
5353 echo "$HOME/.local/bin" >> "$GITHUB_PATH"
5454
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+
5561 - name : Configure wolfSSL (shared + static)
5662 run : autoreconf -ivf && ./configure --enable-shared --enable-static
5763
7379 from cyclonedx.schema import SchemaVersion
7480 v = JsonStrictValidator(SchemaVersion.V1_6)
7581 for path in glob.glob('wolfssl-*.cdx.json'):
76- errors = v.validate_str(open(path).read())
82+ with open(path) as f:
83+ errors = v.validate_str(f.read())
7784 if errors:
7885 print(f"INVALID: {path}: {errors}", file=sys.stderr)
7986 sys.exit(1)
@@ -84,19 +91,29 @@ jobs:
8491
8592 - name : Library hash matches the SBOM
8693 # `make sbom` cleans its private staging tree on exit, so we install
87- # to an independent prefix and re-hash the resulting library. The
88- # autotools install is deterministic (identical bytes), so the hash
89- # the SBOM recorded must match.
94+ # to an independent prefix and re-hash the resulting library.
95+ # Search order matches gen-sbom's so we hash the same artefact.
9096 run : |
9197 rm -rf /tmp/_inst
9298 make install DESTDIR=/tmp/_inst >/dev/null
93- LIB=$(ls /tmp/_inst/usr/local/lib/libwolfssl.so* 2>/dev/null \
94- | grep -v '\.la$' | head -1)
95- test -n "$LIB" || (echo "no installed shared lib"; exit 1)
96- EXPECTED=$(sha256sum "$LIB" | cut -d' ' -f1)
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")
97113 ACTUAL=$(python3 -c "
98114 import json, glob
99- d = json.load(open(glob.glob('wolfssl-*.spdx.json')[0]))
115+ with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
116+ d = json.load(f)
100117 p = [x for x in d['packages'] if x['name'] == 'wolfssl'][0]
101118 print(p['checksums'][0]['checksumValue'])")
102119 test "$EXPECTED" = "$ACTUAL" || \
@@ -108,7 +125,8 @@ jobs:
108125 run : |
109126 python3 - <<'PY'
110127 import glob, json, re, sys
111- d = json.load(open(glob.glob('wolfssl-*.spdx.json')[0]))
128+ with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
129+ d = json.load(f)
112130 refs = {r['referenceType']: r['referenceLocator']
113131 for r in d['packages'][0]['externalRefs']}
114132 assert re.match(r'cpe:2\.3:a:wolfssl:wolfssl:[\d.]+:', refs['cpe23Type']), refs
@@ -141,11 +159,13 @@ jobs:
141159 make sbom
142160 python3 - <<'PY'
143161 import glob, json
144- d = json.load(open(glob.glob('wolfssl-*.spdx.json')[0]))
162+ with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
163+ d = json.load(f)
145164 assert d['packages'][0]['licenseConcluded'].startswith('GPL-3.0-'), \
146165 d['packages'][0]['licenseConcluded']
147166 assert 'hasExtractedLicensingInfos' not in d
148- cdx = json.load(open(glob.glob('wolfssl-*.cdx.json')[0]))
167+ with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
168+ cdx = json.load(f)
149169 lic = cdx['metadata']['component']['licenses']
150170 assert lic == [{'license': {'id': d['packages'][0]['licenseConcluded']}}], lic
151171 print('default GPL: ok ->', lic)
@@ -156,42 +176,67 @@ jobs:
156176 rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
157177 make sbom \
158178 SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial \
159- SBOM_LICENSE_TEXT="$PWD/COPYING"
179+ SBOM_LICENSE_TEXT=/tmp/sbom-fixture-licence.txt
160180 python3 - <<'PY'
161181 import glob, json
162- expected = open('COPYING').read()
163- d = json.load(open(glob.glob('wolfssl-*.spdx.json')[0]))
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)
164186 infos = d['hasExtractedLicensingInfos']
165187 assert len(infos) == 1
166188 assert infos[0]['licenseId'] == 'LicenseRef-wolfSSL-Commercial'
167189 assert infos[0]['extractedText'] == expected
168- cdx = json.load(open(glob.glob('wolfssl-*.cdx.json')[0]))
190+ with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
191+ cdx = json.load(f)
169192 lic = cdx['metadata']['component']['licenses'][0]['license']
170193 assert lic['name'] == 'LicenseRef-wolfSSL-Commercial'
171194 assert lic['text']['content'] == expected
172195 print('LicenseRef + text: ok')
173196 PY
174197 # The output of this run must still pass NTIA and CDX validators.
175198 ntia-checker -c ntia wolfssl-*.spdx.json
176- python3 -c "
199+ python3 - <<'PY'
200+ import glob, sys
177201 from cyclonedx.validation.json import JsonStrictValidator
178202 from cyclonedx.schema import SchemaVersion
179- import glob, sys
180203 v = JsonStrictValidator(SchemaVersion.V1_6)
181- errs = v.validate_str(open(glob.glob('wolfssl-*.cdx.json')[0]).read())
182- sys.exit(1 if errs else 0)"
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+ test ! -f wolfssl-5.9.1.spdx.json || \
224+ { echo "FAIL: SBOM file should not exist after refusal"; \
225+ exit 1; }
183226
184227 - name : License matrix - compound expression
185228 run : |
186229 rm -f wolfssl-*.cdx.json wolfssl-*.spdx.json wolfssl-*.spdx
187230 make sbom \
188231 SBOM_LICENSE_OVERRIDE='GPL-3.0-only OR LicenseRef-wolfSSL-Commercial' \
189- SBOM_LICENSE_TEXT="$PWD/COPYING"
232+ SBOM_LICENSE_TEXT=/tmp/sbom-fixture-licence.txt
190233 python3 - <<'PY'
191234 import glob, json
192- d = json.load(open(glob.glob('wolfssl-*.spdx.json')[0]))
235+ with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
236+ d = json.load(f)
193237 assert len(d['hasExtractedLicensingInfos']) == 1
194- cdx = json.load(open(glob.glob('wolfssl-*.cdx.json')[0]))
238+ with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
239+ cdx = json.load(f)
195240 entry = cdx['metadata']['component']['licenses'][0]
196241 assert 'expression' in entry, entry
197242 print('compound expression: ok')
@@ -203,9 +248,11 @@ jobs:
203248 make sbom SBOM_LICENSE_OVERRIDE=Apache-2.0
204249 python3 - <<'PY'
205250 import glob, json
206- d = json.load(open(glob.glob('wolfssl-*.spdx.json')[0]))
251+ with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
252+ d = json.load(f)
207253 assert 'hasExtractedLicensingInfos' not in d
208- cdx = json.load(open(glob.glob('wolfssl-*.cdx.json')[0]))
254+ with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
255+ cdx = json.load(f)
209256 lic = cdx['metadata']['component']['licenses'][0]['license']
210257 assert lic == {'id': 'Apache-2.0'}, lic
211258 print('simple SPDX override: ok')
@@ -243,3 +290,46 @@ jobs:
243290 echo "uninstall-hook did not remove SBOM artefacts"
244291 exit 1
245292 fi
293+
294+ # Tier 2 (macOS) - smoke test that gen-sbom finds .dylib artefacts and
295+ # that the autotools target works on Mach-O. Linux already exercises
296+ # the heavy validation matrix; this job is intentionally minimal so the
297+ # macOS runner minutes go to portability coverage, not duplicated checks.
298+ integration-macos :
299+ name : SBOM integration (macos)
300+ if : github.repository_owner == 'wolfssl'
301+ runs-on : macos-latest
302+ needs : unit
303+ timeout-minutes : 20
304+ steps :
305+ - uses : actions/checkout@v4
306+
307+ - name : Install build deps and SBOM validators
308+ run : |
309+ brew install autoconf automake libtool
310+ python3 -m pip install --user --break-system-packages \
311+ 'spdx-tools==0.8.*'
312+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
313+ # On some macOS runners pyspdxtools lands in
314+ # Library/Python/<ver>/bin; symlink to a known-on-PATH location.
315+ for d in "$HOME/Library/Python"/*/bin; do
316+ [ -x "$d/pyspdxtools" ] && \
317+ echo "$d" >> "$GITHUB_PATH"
318+ done
319+
320+ - name : Configure wolfSSL (shared)
321+ run : autoreconf -ivf && ./configure --enable-shared
322+
323+ - name : Build + generate SBOM (verifies .dylib detection)
324+ run : make sbom
325+
326+ - name : SBOM hashed a real .dylib
327+ run : |
328+ python3 - <<'PY'
329+ import glob, json, re
330+ with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
331+ d = json.load(f)
332+ checksum = d['packages'][0]['checksums'][0]['checksumValue']
333+ assert re.fullmatch(r'[0-9a-f]{64}', checksum), checksum
334+ print('macOS SBOM checksum well-formed:', checksum)
335+ PY
0 commit comments