mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-08-11 08:41:20 +02:00
feat: SBOM generation and OmniBOR build provenance (CRA compliance)
Add tooling to produce Software Bills of Materials and build provenance for wolfSSL, supporting EU Cyber Resilience Act (CRA) obligations. SBOM generation: - New `make sbom` target producing SPDX 2.3 output with NTIA minimum elements, urn:uuid document namespaces, and SPDX LicenseRef compliance. - Reproducible library discovery across autotools and CMake builds, with liboqs recorded as a linked artefact. - Standalone `scripts/gen-sbom` for embedded / RTOS / custom-builder flows that do not use the main build system, plus --srcs-file, --no-artifact-hash, and hash-source options. Build provenance (OmniBOR / bomsh): - End-to-end bomsh tracing of the built binaries with ArtifactID insertion, snapshotting the traced library before libtool relink and hashing the bomsh-traced binary. - `scripts/bomsh_verify.py` to validate provenance against the traced gitoid. Security advisories: - `scripts/gen-advisory` generating CSAF 2.0 and CycloneDX VEX, with a `make` target, VEX overlay schema/example, and CWE name data. Docs, tests, and CI: - doc/SBOM.md and doc/CRA.md, plus README/INSTALL updates. - Unit and regression tests for gen-sbom and gen-advisory. - New sbom.yml and advisory.yml workflows: SPDX validation via pyspdxtools, CSAF validation, bomsh provenance verification, SBOM artifact archiving, macOS coverage, and actions pinned to SHAs. Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
This commit is contained in:
committed by
Mark Atwood
parent
f5ace71dd3
commit
4ec80d309a
@@ -0,0 +1,217 @@
|
||||
name: Advisory Tests
|
||||
|
||||
# START OF COMMON SECTION
|
||||
on:
|
||||
push:
|
||||
branches: [ 'master', 'main', 'release/**' ]
|
||||
pull_request:
|
||||
branches: [ '*' ]
|
||||
|
||||
# Defence-in-depth: this workflow only reads the tree and validates generated
|
||||
# advisories (no API writes, no git push, no release upload), so pin the token
|
||||
# to read-only per GitHub's supply-chain hardening guidance.
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
# END OF COMMON SECTION
|
||||
|
||||
jobs:
|
||||
# Tier 1 - pure-Python unit + semantic tests for scripts/gen-advisory.
|
||||
# No build, no pip deps. Runs in seconds and is the cheapest gate for the
|
||||
# record->model logic and the CSAF semantic invariants (every product_id
|
||||
# defined/used, no contradicting status, flags only on not-affected
|
||||
# products, no cvss_v4 in CSAF 2.0 scores, canonical CWE names, ...).
|
||||
unit:
|
||||
name: gen-advisory unit tests
|
||||
if: github.repository_owner == 'wolfssl'
|
||||
runs-on: ubuntu-24.04
|
||||
timeout-minutes: 5
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Syntax check
|
||||
run: python3 -m py_compile scripts/gen-advisory
|
||||
|
||||
- name: Unit tests
|
||||
run: python3 -W error::ResourceWarning -m unittest scripts/test_gen_advisory.py -v
|
||||
|
||||
# Tier 2 - format-level validation: generate per-CVE and bundled advisories
|
||||
# from the committed CVE fixtures + example overlay, then validate the
|
||||
# CycloneDX VEX against the 1.6 strict schema (same validator the SBOM
|
||||
# workflow uses) and the VEX overlay against its JSON Schema. Also pins
|
||||
# SOURCE_DATE_EPOCH reproducibility for both emitters.
|
||||
schema:
|
||||
name: advisory schema validation
|
||||
if: github.repository_owner == 'wolfssl'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: unit
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install validators
|
||||
# cyclonedx-bom provides the CycloneDX 1.6 strict JSON validator (same
|
||||
# pin as .github/workflows/sbom.yml); jsonschema validates the VEX
|
||||
# overlay against scripts/advisory-vex-overlay.schema.json. Pinned so
|
||||
# a validator release cannot silently change what "valid" means.
|
||||
run: |
|
||||
python3 -m pip install --user --upgrade pip
|
||||
python3 -m pip install --user 'cyclonedx-bom==7.*' 'jsonschema==4.*'
|
||||
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||
|
||||
- name: Overlay validates against its JSON Schema
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import json, jsonschema
|
||||
schema = json.load(open('scripts/advisory-vex-overlay.schema.json'))
|
||||
overlay = json.load(open('scripts/advisory-vex-overlay.example.json'))
|
||||
jsonschema.Draft202012Validator.check_schema(schema)
|
||||
jsonschema.Draft202012Validator(schema).validate(overlay)
|
||||
print('OK: example overlay matches advisory-vex-overlay.schema.json')
|
||||
PY
|
||||
|
||||
- name: Generate advisories (per-CVE + bundled)
|
||||
# Mirrors how a release would be cut: one document per CVE, plus a
|
||||
# bundled per-release advisory carrying both. SOURCE_DATE_EPOCH makes
|
||||
# the run deterministic for the reproducibility check below.
|
||||
run: |
|
||||
mkdir -p /tmp/adv
|
||||
for id in CVE-2026-5501 CVE-2026-5778 CVE-2026-5999; do
|
||||
SOURCE_DATE_EPOCH=1700000000 \
|
||||
python3 scripts/gen-advisory \
|
||||
--cve-record "scripts/testdata/$id.json" \
|
||||
--vex-overlay scripts/advisory-vex-overlay.example.json \
|
||||
--csaf-out "/tmp/adv/$id.csaf.json" \
|
||||
--cdx-vex-out "/tmp/adv/$id.cdx.json"
|
||||
done
|
||||
SOURCE_DATE_EPOCH=1700000000 \
|
||||
python3 scripts/gen-advisory \
|
||||
--cve-record scripts/testdata/CVE-2026-5501.json \
|
||||
--cve-record scripts/testdata/CVE-2026-5778.json \
|
||||
--vex-overlay scripts/advisory-vex-overlay.example.json \
|
||||
--advisory-id wolfSSL-SA-5.9.1 \
|
||||
--csaf-out /tmp/adv/wolfSSL-SA-5.9.1.csaf.json \
|
||||
--cdx-vex-out /tmp/adv/wolfSSL-SA-5.9.1.cdx.json
|
||||
|
||||
- name: CycloneDX VEX validates per CycloneDX 1.6 strict schema
|
||||
run: |
|
||||
python3 - <<'PY'
|
||||
import glob, sys
|
||||
from cyclonedx.validation.json import JsonStrictValidator
|
||||
from cyclonedx.schema import SchemaVersion
|
||||
v = JsonStrictValidator(SchemaVersion.V1_6)
|
||||
paths = sorted(glob.glob('/tmp/adv/*.cdx.json'))
|
||||
assert paths, 'no CycloneDX VEX documents were generated'
|
||||
for p in paths:
|
||||
errs = v.validate_str(open(p).read())
|
||||
if errs:
|
||||
print(f'INVALID: {p}: {errs}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f'OK: {p}')
|
||||
PY
|
||||
|
||||
- name: Reproducibility - two runs are byte-identical
|
||||
run: |
|
||||
mkdir -p /tmp/adv-r2
|
||||
SOURCE_DATE_EPOCH=1700000000 \
|
||||
python3 scripts/gen-advisory \
|
||||
--cve-record scripts/testdata/CVE-2026-5501.json \
|
||||
--cve-record scripts/testdata/CVE-2026-5778.json \
|
||||
--vex-overlay scripts/advisory-vex-overlay.example.json \
|
||||
--advisory-id wolfSSL-SA-5.9.1 \
|
||||
--csaf-out /tmp/adv-r2/wolfSSL-SA-5.9.1.csaf.json \
|
||||
--cdx-vex-out /tmp/adv-r2/wolfSSL-SA-5.9.1.cdx.json
|
||||
diff /tmp/adv/wolfSSL-SA-5.9.1.csaf.json \
|
||||
/tmp/adv-r2/wolfSSL-SA-5.9.1.csaf.json
|
||||
diff /tmp/adv/wolfSSL-SA-5.9.1.cdx.json \
|
||||
/tmp/adv-r2/wolfSSL-SA-5.9.1.cdx.json
|
||||
|
||||
- name: Default/batch path matches `make advisory`
|
||||
# No record flags: gen-advisory falls back to the canonical
|
||||
# advisories/ tree (the exact inputs `make advisory` feeds it via
|
||||
# --records-dir/--vex-overlay), proving the script and the build target
|
||||
# are interchangeable and that the committed real records + overlay
|
||||
# generate and validate.
|
||||
run: |
|
||||
python3 scripts/gen-advisory --out-dir /tmp/adv-default
|
||||
python3 - <<'PY'
|
||||
import glob, sys
|
||||
from cyclonedx.validation.json import JsonStrictValidator
|
||||
from cyclonedx.schema import SchemaVersion
|
||||
v = JsonStrictValidator(SchemaVersion.V1_6)
|
||||
paths = sorted(glob.glob('/tmp/adv-default/*.cdx.json'))
|
||||
assert paths, 'batch mode produced no CycloneDX documents'
|
||||
for p in paths:
|
||||
errs = v.validate_str(open(p).read())
|
||||
if errs:
|
||||
print(f'INVALID: {p}: {errs}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
print(f'OK: {p}')
|
||||
PY
|
||||
|
||||
- name: Upload generated advisories
|
||||
if: always()
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: advisories-${{ github.sha }}
|
||||
path: /tmp/adv/*.json
|
||||
if-no-files-found: warn
|
||||
retention-days: 90
|
||||
|
||||
# Tier 2 - CSAF 2.0 conformance: the real gate. JSON-schema validity is
|
||||
# necessary but not sufficient; CSAF defines mandatory tests (section 6.1.*)
|
||||
# -- CVSS/vector consistency, contradicting product status, product_id
|
||||
# defined/used, tracking.version vs revision_history, CWE name match, ... --
|
||||
# that a bare schema pass accepts. scripts/csaf_validate.mjs runs the strict
|
||||
# 2.0 schema + all mandatory tests via the Secvisogram reference
|
||||
# implementation (bundles every schema incl. the first.org CVSS schemas, so
|
||||
# it is fully offline once installed).
|
||||
csaf-conformance:
|
||||
name: CSAF 2.0 mandatory tests
|
||||
if: github.repository_owner == 'wolfssl'
|
||||
runs-on: ubuntu-24.04
|
||||
needs: unit
|
||||
timeout-minutes: 10
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install csaf-validator-lib (pinned)
|
||||
# Pinned: csaf-validator-lib implements the CSAF mandatory tests, and
|
||||
# an unpinned upgrade could change pass/fail semantics under us. The
|
||||
# bare `csaf-validator-lib` name on npm is an unrelated placeholder;
|
||||
# the reference implementation is the @secvisogram scope.
|
||||
run: npm install --no-save @secvisogram/csaf-validator-lib@2.0.25
|
||||
|
||||
- name: Generate CSAF advisories (per-CVE + bundled)
|
||||
run: |
|
||||
mkdir -p /tmp/adv
|
||||
for id in CVE-2026-5501 CVE-2026-5778 CVE-2026-5999; do
|
||||
python3 scripts/gen-advisory \
|
||||
--cve-record "scripts/testdata/$id.json" \
|
||||
--vex-overlay scripts/advisory-vex-overlay.example.json \
|
||||
--csaf-out "/tmp/adv/$id.csaf.json"
|
||||
done
|
||||
python3 scripts/gen-advisory \
|
||||
--cve-record scripts/testdata/CVE-2026-5501.json \
|
||||
--cve-record scripts/testdata/CVE-2026-5778.json \
|
||||
--vex-overlay scripts/advisory-vex-overlay.example.json \
|
||||
--advisory-id wolfSSL-SA-5.9.1 \
|
||||
--csaf-out /tmp/adv/wolfSSL-SA-5.9.1.csaf.json
|
||||
|
||||
- name: CSAF strict schema + mandatory tests
|
||||
run: node scripts/csaf_validate.mjs /tmp/adv/*.csaf.json
|
||||
|
||||
- name: CSAF default/batch path (canonical advisories/ tree)
|
||||
# Same conformance gate, but driven through the zero-argument default
|
||||
# path `make advisory` uses, against the committed real records +
|
||||
# advisories/vex-overlay.json.
|
||||
run: |
|
||||
python3 scripts/gen-advisory --out-dir /tmp/adv-default
|
||||
node scripts/csaf_validate.mjs /tmp/adv-default/*.csaf.json
|
||||
@@ -27,8 +27,8 @@ jobs:
|
||||
check_filenames: true
|
||||
check_hidden: true
|
||||
# Add comma separated list of words that occur multiple times that should be ignored (sorted alphabetically, case sensitive)
|
||||
ignore_words_list: adin,ameba,aNULL,brunch,carryIn,chainG,ciph,cLen,cliKs,dout,FPR,fpr,haveA,inCreated,inOut,inout,larg,LEAPYEAR,Merget,optionA,parm,parms,repid,rIn,userA,ser,siz,te,Te,HSI,failT,toLen,vor,
|
||||
ignore_words_list: adin,ameba,aNULL,brunch,carryIn,chainG,ciph,cLen,cliKs,cna,dout,FPR,fpr,haveA,inCreated,inOut,inout,larg,LEAPYEAR,Merget,optionA,parm,parms,repid,rIn,userA,ser,siz,te,Te,HSI,failT,toLen,vor,
|
||||
# The exclude_file contains lines of code that should be ignored. This is useful for individual lines which have non-words that can safely be ignored.
|
||||
exclude_file: '.codespellexcludelines'
|
||||
# To skip files entirely from being processed, add it to the following list:
|
||||
skip: '*.cproject,*.csr,*.der,*.mtpj,*.pem,*.vcxproj,.git,*.launch,*.scfg,*.revoked,./examples/asn1/dumpasn1.cfg,./examples/asn1/oid_names.h'
|
||||
skip: '*.cproject,*.csr,*.der,*.mtpj,*.pem,*.vcxproj,.git,*.launch,*.scfg,*.revoked,./examples/asn1/dumpasn1.cfg,./examples/asn1/oid_names.h,./scripts/cwe-names.json'
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user