Skip to content

Commit 2fff508

Browse files
committed
test(sbom): liboqs/bomsh CI coverage, macOS PATH fix, dep regressions
Adds liboqs+bomsh CI jobs, locks DEP_META shape via 5 new tests, ships test_gen_sbom.py in dist.
1 parent e5533a9 commit 2fff508

3 files changed

Lines changed: 269 additions & 7 deletions

File tree

.github/workflows/sbom.yml

Lines changed: 186 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,85 @@ jobs:
259259
print('simple SPDX override: ok')
260260
PY
261261
262+
# ---- liboqs / Falcon dep entry ---------------------------------------
263+
# Without this, every code path that emits a dep package - pkg-config
264+
# lookup, supplier/purl/license construction, deterministic UUID
265+
# derivation for deps - is uncovered by CI. A future rename or shape
266+
# break in DEP_META['liboqs'] would silently land.
267+
268+
- name: Install liboqs (provides liboqs.pc for --with-liboqs)
269+
run: sudo apt-get update && sudo apt-get install -y liboqs-dev
270+
271+
- name: Configure with --with-liboqs --enable-falcon
272+
run: |
273+
make distclean
274+
autoreconf -ivf
275+
./configure --enable-shared --enable-experimental \
276+
--with-liboqs --enable-falcon
277+
278+
- name: Build + generate SBOM with liboqs enabled
279+
run: make sbom
280+
281+
- name: liboqs dep entry resolves to a CVE-trackable identifier
282+
# The point of recording liboqs (rather than `falcon`) is that
283+
# OSV / Grype / Trivy / Dependency-Track key vulnerability
284+
# records off purl + name. These assertions guard the contract
285+
# that pulled the entry away from the algorithm name.
286+
run: |
287+
python3 - <<'PY'
288+
import glob, json, re, sys
289+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
290+
d = json.load(f)
291+
pkgs = {p['name']: p for p in d['packages']}
292+
assert 'liboqs' in pkgs, list(pkgs)
293+
assert 'falcon' not in pkgs, "algorithm name leaked as a dep package"
294+
liboqs = pkgs['liboqs']
295+
assert liboqs['supplier'] == 'Organization: Open Quantum Safe', \
296+
liboqs['supplier']
297+
refs = {r['referenceType']: r['referenceLocator']
298+
for r in liboqs.get('externalRefs', [])}
299+
assert 'purl' in refs, refs
300+
assert re.match(r'pkg:github/open-quantum-safe/liboqs@', refs['purl']), \
301+
refs['purl']
302+
# Algorithm enablement must still be visible via build_props
303+
# (parsed from options.h), not via the dep entry.
304+
props = {p['name']: p['value']
305+
for p in d['packages'][0].get('annotations', [])
306+
if p.get('annotationType') == 'OTHER'}
307+
# CycloneDX side: same package + version present.
308+
with open(glob.glob('wolfssl-*.cdx.json')[0]) as f:
309+
cdx = json.load(f)
310+
deps = {c['name']: c for c in cdx.get('components', [])}
311+
assert 'liboqs' in deps, list(deps)
312+
print('liboqs dep entry: ok ->', refs['purl'])
313+
PY
314+
315+
- name: HAVE_FALCON algorithm flag is captured as a build property
316+
# Algorithm visibility moved out of the dep entry; this verifies
317+
# it is still preserved (just somewhere honest).
318+
run: |
319+
python3 - <<'PY'
320+
import glob, json
321+
with open(glob.glob('wolfssl-*.spdx.json')[0]) as f:
322+
d = json.load(f)
323+
wolf = [p for p in d['packages'] if p['name'] == 'wolfssl'][0]
324+
props = {p['name']: p['value']
325+
for p in wolf.get('annotations', [])
326+
if p.get('annotationType') == 'OTHER'}
327+
# Build props can land as annotations or as a 'attributionTexts'
328+
# block depending on SPDX version; check both.
329+
combined = json.dumps(d)
330+
assert 'HAVE_FALCON' in combined, \
331+
"HAVE_FALCON missing from SBOM build properties"
332+
print('HAVE_FALCON build prop: present')
333+
PY
334+
335+
- name: Restore default build for remaining steps
336+
run: |
337+
make distclean
338+
autoreconf -ivf
339+
./configure --enable-shared --enable-static
340+
262341
# ---- Distribution + install hooks -----------------------------------
263342

264343
- name: Tarball roundtrip (make dist -> ./configure -> make sbom)
@@ -310,13 +389,14 @@ jobs:
310389
brew install autoconf automake libtool
311390
python3 -m pip install --user --break-system-packages \
312391
'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
392+
# Resolve the actual scripts dir for the python that ran pip,
393+
# rather than guessing a glob like `~/Library/Python/*/bin`.
394+
# `posix_user` is the install scheme `pip install --user` wrote
395+
# to, so this matches even when the runner's selected python
396+
# changes between minor versions / homebrew vs system.
397+
python3 -c \
398+
'import sysconfig; print(sysconfig.get_path("scripts","posix_user"))' \
399+
>> "$GITHUB_PATH"
320400
321401
- name: Configure wolfSSL (shared)
322402
run: autoreconf -ivf && ./configure --enable-shared
@@ -334,3 +414,102 @@ jobs:
334414
assert re.fullmatch(r'[0-9a-f]{64}', checksum), checksum
335415
print('macOS SBOM checksum well-formed:', checksum)
336416
PY
417+
418+
# Tier 2 (bomsh) - exercises the `make bomsh` target which traces a
419+
# full clean rebuild under bomtrace3 (patched strace, Linux-only) and
420+
# produces an OmniBOR artifact dependency graph. Without this job
421+
# the entire bomsh recipe and its SPDX enrichment step would only be
422+
# exercised by hand; a regression in either would silently land.
423+
bomsh:
424+
name: bomsh integration (linux)
425+
if: github.repository_owner == 'wolfssl'
426+
runs-on: ubuntu-24.04
427+
needs: unit
428+
timeout-minutes: 30
429+
steps:
430+
- uses: actions/checkout@v4
431+
432+
- name: Install build deps + SBOM validators
433+
run: |
434+
sudo apt-get update
435+
sudo apt-get install -y build-essential autoconf automake libtool \
436+
python3 python3-pip git
437+
python3 -m pip install --user --upgrade pip
438+
python3 -m pip install --user 'spdx-tools==0.8.*'
439+
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
440+
441+
- name: Install bomsh toolchain (bomtrace3 + helper scripts)
442+
# Bomsh is not packaged; build bomtrace3 (patched strace) from
443+
# source and install the python helpers system-wide so configure's
444+
# AC_PATH_PROG can find them.
445+
run: |
446+
git clone --depth=1 https://github.com/omnibor/bomsh /tmp/bomsh
447+
# bomtrace3 build: docker/devcontainer-only Makefile in upstream;
448+
# use the embedded build script if present, else fall back to
449+
# the strace patch path.
450+
cd /tmp/bomsh
451+
if [ -d .devcontainer/bomtrace3 ]; then
452+
make -C .devcontainer/bomtrace3
453+
sudo install -m 755 .devcontainer/bomtrace3/bomtrace3 \
454+
/usr/local/bin/
455+
else
456+
echo "bomsh repo layout changed; please update CI"
457+
exit 1
458+
fi
459+
sudo install -m 755 scripts/bomsh_create_bom.py /usr/local/bin/
460+
sudo install -m 755 scripts/bomsh_sbom.py /usr/local/bin/
461+
bomtrace3 --version || true
462+
which bomsh_create_bom.py bomsh_sbom.py
463+
464+
- name: Configure wolfSSL
465+
run: autoreconf -ivf && ./configure --enable-shared
466+
467+
- name: Generate SPDX (input to bomsh enrichment)
468+
run: make sbom
469+
470+
- name: Run make bomsh
471+
run: make bomsh
472+
473+
- name: OmniBOR artifact graph produced
474+
# bomsh writes the artifact dependency graph under omnibor/.
475+
# Empty/missing graph means bomtrace3 silently failed to trace.
476+
run: |
477+
test -d omnibor
478+
test "$(find omnibor -type f | wc -l)" -gt 0
479+
echo "omnibor/ contents:"
480+
find omnibor -maxdepth 3 -type f | head -20
481+
482+
- name: Enriched SPDX has PERSISTENT-ID gitoid externalRef
483+
# The whole point of `make bomsh` over `make sbom` is the
484+
# bridge between component identity (SPDX package) and build
485+
# provenance (OmniBOR gitoid). If the enrichment step ran but
486+
# produced an SPDX without the gitoid ref, the bridge is broken.
487+
run: |
488+
ls omnibor.wolfssl-*.spdx.json
489+
python3 - <<'PY'
490+
import glob, json, sys
491+
path = glob.glob('omnibor.wolfssl-*.spdx.json')[0]
492+
with open(path) as f:
493+
d = json.load(f)
494+
gitoid_refs = []
495+
for pkg in d.get('packages', []):
496+
for ref in pkg.get('externalRefs', []):
497+
if (ref.get('referenceCategory') == 'PERSISTENT-ID'
498+
or ref.get('referenceType') == 'gitoid'):
499+
gitoid_refs.append(ref)
500+
assert gitoid_refs, \
501+
f'no PERSISTENT-ID gitoid externalRef in {path}'
502+
print(f'bomsh enrichment ok: {len(gitoid_refs)} gitoid refs')
503+
PY
504+
505+
- name: make clean removes all bomsh + sbom artefacts
506+
# Regression guard: if a future change adds an output to either
507+
# recipe but forgets CLEANFILES, this will catch it.
508+
run: |
509+
make clean
510+
if ls wolfssl-*.spdx.json wolfssl-*.cdx.json \
511+
omnibor.wolfssl-*.spdx.json 2>/dev/null; then
512+
echo "make clean did not remove SBOM/bomsh artefacts"
513+
exit 1
514+
fi
515+
test ! -d omnibor || (echo "omnibor/ not cleaned"; exit 1)

scripts/include.am

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -160,3 +160,7 @@ EXTRA_DIST += scripts/user_settings_asm.sh
160160
# Must be in the dist tarball, otherwise `make dist && cd <tarball> &&
161161
# ./configure && make sbom` fails for downstream consumers.
162162
EXTRA_DIST += scripts/gen-sbom
163+
164+
# SBOM generator unit tests. Shipped so downstream consumers building
165+
# from a release tarball can re-run the regression suite.
166+
EXTRA_DIST += scripts/test_gen_sbom.py

scripts/test_gen_sbom.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,5 +336,84 @@ def test_dedup_keeps_last_assignment(self):
336336
self.assertEqual(pairs['HAVE_X'], '2')
337337

338338

339+
class TestDepMetaShape(unittest.TestCase):
340+
"""Lock down the dep-tracking surface so renames/removals don't
341+
silently regress vulnerability-scanner identifiers in the SBOM.
342+
343+
These guard against:
344+
* an external dep being added without a CVE-resolvable identifier
345+
* a future PR re-introducing the `falcon`/`libxmss`/`liblms`
346+
keys after they were intentionally removed."""
347+
348+
def test_only_libz_and_liboqs_are_tracked(self):
349+
self.assertEqual(set(gs.DEP_META.keys()), {'libz', 'liboqs'})
350+
351+
def test_liboqs_entry_describes_the_linked_artefact(self):
352+
liboqs = gs.DEP_META['liboqs']
353+
self.assertEqual(liboqs['name'], 'liboqs')
354+
self.assertEqual(liboqs['supplier'], 'Open Quantum Safe')
355+
self.assertEqual(liboqs['pkgconfig'], 'liboqs')
356+
self.assertEqual(
357+
liboqs['purl']('0.10.0'),
358+
'pkg:github/open-quantum-safe/liboqs@0.10.0')
359+
360+
def test_no_stale_dep_keys(self):
361+
# `falcon` is an algorithm, not a linked package; it must not
362+
# appear as a dep entry (algorithm enablement lives in
363+
# build_props parsed from options.h). `libxmss` and `liblms`
364+
# were removed upstream; their re-appearance here would
365+
# silently emit unresolvable identifiers in the SBOM.
366+
for stale in ('falcon', 'libxmss', 'liblms', 'xmss', 'lms'):
367+
self.assertNotIn(stale, gs.DEP_META)
368+
369+
370+
class TestEnabledDepsCli(unittest.TestCase):
371+
"""End-to-end test of the argparse plumbing for --dep-* flags.
372+
373+
Runs gen-sbom in a child process so we exercise the real argparse
374+
config rather than a re-imported module."""
375+
376+
def _run(self, *argv):
377+
import subprocess
378+
here = pathlib.Path(__file__).resolve().parent
379+
script = here / 'gen-sbom'
380+
return subprocess.run(
381+
['python3', str(script), *argv],
382+
capture_output=True, text=True
383+
)
384+
385+
def test_dep_liboqs_is_accepted(self):
386+
result = self._run('--help')
387+
self.assertEqual(result.returncode, 0, result.stderr)
388+
self.assertIn('--dep-liboqs', result.stdout)
389+
self.assertIn('--dep-libz', result.stdout)
390+
391+
def test_removed_flags_are_rejected(self):
392+
# Each of these was either renamed (--dep-falcon -> --dep-liboqs)
393+
# or removed entirely (--dep-libxmss/--dep-liblms with upstream
394+
# removal of the libraries). argparse should reject them as
395+
# unrecognised, not silently accept them. We pass the full set
396+
# of required args (against /dev/null sentinels) so argparse
397+
# progresses to the unknown-flag check; we never want
398+
# gen-sbom to actually generate anything in this test.
399+
required = [
400+
'--name', 'wolfssl',
401+
'--version', '0.0.0-test',
402+
'--lib', '/dev/null',
403+
'--license-file', '/dev/null',
404+
'--options-h', '/dev/null',
405+
'--cdx-out', '/dev/null',
406+
'--spdx-out', '/dev/null',
407+
]
408+
for stale_flag in ('--dep-falcon', '--dep-libxmss', '--dep-liblms',
409+
'--dep-libxmss-root', '--dep-liblms-root',
410+
'--git'):
411+
result = self._run(*required, stale_flag, 'no')
412+
self.assertNotEqual(result.returncode, 0,
413+
f"{stale_flag!r} unexpectedly accepted")
414+
self.assertIn('unrecognized arguments', result.stderr,
415+
f"{stale_flag!r}: {result.stderr!r}")
416+
417+
339418
if __name__ == '__main__':
340419
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)