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:
Sameeh Jubran
2026-06-30 14:13:57 +03:00
committed by Mark Atwood
parent f5ace71dd3
commit 4ec80d309a
28 changed files with 10557 additions and 3 deletions
+217
View File
@@ -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
+2 -2
View File
@@ -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
+22
View File
@@ -9,6 +9,28 @@ ctaocrypt/src/src/
*.cache
*.su
.dirstamp
# SBOM / bomsh output artefacts (produced by `make sbom` / `make bomsh`).
# Built per-release, not source. Listed first so they survive any
# subsequent `!`-style un-ignore patterns added below.
/wolfssl-*.cdx.json
/wolfssl-*.spdx.json
/wolfssl-*.spdx
/omnibor.wolfssl-*.spdx.json
/omnibor/
/_sbom_staging/
/_bomsh.conf
/bomsh_raw_logfile*
# Generated advisory documents (produced by `make advisory` /
# `scripts/gen-advisory`). Inputs under advisories/records/ and
# advisories/vex-overlay.json are tracked; the generated out/ tree is not.
/advisories/out/
# Node deps pulled in only by the CSAF conformance check
# (scripts/csaf_validate.mjs uses @secvisogram/csaf-validator-lib).
/node_modules/
/package-lock.json
*.user
!*-VS2022.vcxproj.user
configure
+161
View File
@@ -315,3 +315,164 @@ We also have vcpkg ports for wolftpm, wolfmqtt and curl.
Docker container, use `make rpm-docker`. In both cases the
resulting packages are placed in the root directory of the
project.
19. Generating an SBOM (Software Bill of Materials)
wolfSSL can generate a Software Bill of Materials for EU Cyber Resilience
Act (CRA) compliance. Two entry points are supported, depending on how
you build wolfSSL.
--- 19a. Embedded / RTOS / IDE-based builds (no autotools) ----------
For customers building wolfSSL from a hand-edited user_settings.h with
their own Makefile, Keil MDK, IAR EWARM, STM32CubeIDE, ESP-IDF,
Zephyr, or plain CMake, invoke scripts/gen-sbom directly. No
./configure, no autotools.
Prerequisites:
- python3
- pcpp (pip install pcpp) # required for --user-settings
- spdx-tools (pip install spdx-tools) # optional; for SPDX validation
Usage:
$ python3 wolfssl/scripts/gen-sbom \
--name wolfssl --version 5.9.1 \
--license-file wolfssl/LICENSING \
--user-settings wolfssl/wolfssl/wolfcrypt/settings.h \
--user-settings-include wolfssl \
--user-settings-include path/to/your/user_settings_dir \
--user-settings-define WOLFSSL_USER_SETTINGS \
--srcs wolfssl/wolfcrypt/src/aes.c [...your wolfssl source list] \
--cdx-out wolfssl-5.9.1.cdx.json \
--spdx-out wolfssl-5.9.1.spdx.json
The component checksum is a deterministic OmniBOR-compatible Merkle
hash over the source files you compile into your firmware, so you do
not need to synthesize a separate libwolfssl.a just for SBOM purposes.
See doc/SBOM.md section 1 for per-toolchain recipes (Keil, IAR,
STM32CubeIDE, ESP-IDF, Zephyr, CMake) and the full flag reference.
--- 19b. Linux / autotools builds ----------------------------------
For Debian, RPM, Yocto, FIPS-Ready, and other builds that already use
./configure && make:
Prerequisites:
- python3 (detected automatically by configure)
- pyspdxtools (pip install spdx-tools)
Usage:
$ ./configure
$ make
$ make sbom
This produces three files in the build directory:
wolfssl-<version>.cdx.json CycloneDX 1.6 JSON
wolfssl-<version>.spdx.json SPDX 2.3 JSON
wolfssl-<version>.spdx SPDX 2.3 tag-value (validated by pyspdxtools)
The SPDX JSON is validated by pyspdxtools before the tag-value file is
written; make sbom fails if validation fails.
`make sbom` is a thin convenience wrapper around the same
scripts/gen-sbom Python entry point that section 19a uses, with all
paths resolved automatically from the autotools build tree.
To install the SBOM files to $(datadir)/doc/wolfssl/:
$ make install-sbom
To remove installed SBOM files:
$ make uninstall-sbom
The generated files are removed by make clean.
For details on the SBOM contents and CRA context, see doc/SBOM.md.
20. Generating OmniBOR build artifact graph (Bomsh)
wolfSSL supports generating an OmniBOR artifact dependency graph using
the Bomsh project (https://github.com/omnibor/bomsh). OmniBOR provides
cryptographic traceability from every binary artifact back to the exact
source files that produced it.
Prerequisites:
- bomtrace3 (build from https://github.com/omnibor/bomsh)
- bomsh_create_bom.py (from the bomsh scripts/ directory, in PATH)
- bomsh_sbom.py (optional; from bomsh scripts/, for SPDX enrichment)
Both bomtrace3 and the Python scripts are detected by configure.
make bomsh fails with a clear error message if either required tool
is missing.
Usage:
$ ./configure
$ make
$ make bomsh
This performs a clean rebuild of wolfSSL under bomtrace3 tracing,
then produces an OmniBOR artifact graph in omnibor/ in the build
directory. If bomsh_sbom.py is available and a wolfssl-<ver>.spdx.json
exists (from 'make sbom'), it also produces an OmniBOR-enriched SPDX
document omnibor.wolfssl-<ver>.spdx.json.
To install:
$ make install-bomsh # installs omnibor/ to $(datadir)/doc/wolfssl/
$ make uninstall-bomsh # removes installed files
The generated files are removed by make clean.
See doc/SBOM.md for full details.
21. Generating security advisories (CSAF 2.0 + CycloneDX VEX)
wolfSSL can generate machine-readable security advisories from a
canonical, git-tracked single source of truth. Each CVE record
produces one CSAF 2.0 document and one CycloneDX 1.6 VEX document,
suitable for downstream vulnerability tooling and CRA reporting.
Prerequisites:
- python3 (detected automatically by configure)
Usage:
$ ./configure
$ make advisory
Inputs (tracked in git):
advisories/records/*.json one JSON record per CVE
advisories/vex-overlay.json shared VEX overlay metadata
Outputs (build artifacts, one pair per record):
advisories/out/*.csaf.json CSAF 2.0 JSON
advisories/out/*.cdx.json CycloneDX 1.6 VEX JSON
Output is reproducible: SOURCE_DATE_EPOCH is honored and defaults to
the last git commit timestamp when unset, exactly like make sbom.
`make advisory` is a thin wrapper around scripts/gen-advisory and is
byte-for-byte interchangeable with running that script by hand:
$ python3 scripts/gen-advisory \
--records-dir advisories/records \
--vex-overlay advisories/vex-overlay.json \
--out-dir advisories/out
To install the advisory files to $(datadir)/doc/wolfssl/advisories/:
$ make install-advisory
To remove installed advisory files:
$ make uninstall-advisory
The generated files are removed by make clean.
+321
View File
@@ -439,3 +439,324 @@ merge-clean:
.cu.lo:
$(LIBTOOL) --tag=CC --mode=compile $(COMPILE) --compile -o $@ $< -static
# SBOM generation (CRA compliance)
SBOM_CDX = wolfssl-$(PACKAGE_VERSION).cdx.json
SBOM_SPDX = wolfssl-$(PACKAGE_VERSION).spdx.json
SBOM_SPDX_TV = wolfssl-$(PACKAGE_VERSION).spdx
sbomdir = $(datadir)/doc/$(PACKAGE)
# Shared-library / Mach-O basenames in priority order (versioned first).
# Both `sbom:` and `bomsh:` glob for these under their own search prefixes;
# adding a new platform-specific dynamic-library extension here updates
# both targets at once. Static (.a) and Windows (.dll/.lib) variants are
# listed inline at each call-site because their ordering and prefixes
# differ between the install tree and the build tree.
WOLFSSL_LIB_DSO_BASENAMES = \
libwolfssl.so.[0-9]* \
libwolfssl.so \
libwolfssl.[0-9]*.dylib \
libwolfssl.dylib
.PHONY: sbom install-sbom uninstall-sbom
# Stage a `make install` into a private tree, discover the installed library
# artifact (shared or static, ELF/Mach-O/PE), hash it, generate SPDX+CDX,
# validate the SPDX, then convert to tag-value. The staging tree is removed
# unconditionally via `trap`, even if any step fails. Honors SOURCE_DATE_EPOCH
# for reproducible builds (set by the recipe to `git log -1 --format=%ct` when
# unset and a git tree is available).
#
# User-overridable variables:
# SBOM_LICENSE_OVERRIDE SPDX expression to use instead of the GPL ID
# parsed from LICENSING (e.g. for commercial
# licensees: LicenseRef-wolfSSL-Commercial).
# SBOM_LICENSE_TEXT Path to the actual licence text for any
# LicenseRef-* in SBOM_LICENSE_OVERRIDE. Required
# for SPDX 2.3 conformance whenever a custom
# LicenseRef is in use; `make sbom` exits with an
# error if it is missing.
# SBOM_DOCUMENT_NAMESPACE Override the SPDX documentNamespace. Default
# is a deterministic urn:uuid (SPDX 2.3 sec. 6.5
# requires only uniqueness, not resolvability).
# Downstream packagers re-hosting the SBOM under
# their own URL should set this to a URI they
# actually serve (e.g.
# https://example.com/sbom/wolfssl-X.Y.Z.spdx.json).
# SBOM_DEP_VERSIONS Space-separated KEY=VERSION list forwarded to
# gen-sbom as repeated --dep-version flags (KEY is
# one of the known deps, e.g. liboqs / libz). Use
# this on build/packaging hosts that lack the dep's
# pkg-config .pc file, where version detection would
# otherwise fall back to NOASSERTION (SPDX) / an
# omitted version+purl (CycloneDX). Example:
# make sbom SBOM_DEP_VERSIONS='liboqs=0.10.0 libz=1.3.1'.
# SBOM_LIB_OVERRIDE Absolute path to the library artefact whose
# SHA-256 should land in the SBOM, INSTEAD of
# discovering one via a private staging install.
# Set by `make bomsh` so the SBOM hash and the
# OmniBOR enrichment refer to the SAME bomsh-
# traced binary; without this override `make
# sbom` would re-link via `make install` and
# hash a different artefact than `bomsh_sbom.py`
# fingerprints, leaving the SHA-256 in
# `checksums[]` and the gitoid in `externalRefs`
# describing two unrelated files.
sbom:
@if test -z "$(PYTHON3)"; then \
echo ""; \
echo "ERROR: 'python3' not found in PATH. Cannot generate SBOM."; \
echo ""; \
exit 1; \
fi
@if test -z "$(PYSPDXTOOLS)"; then \
echo ""; \
echo "ERROR: 'pyspdxtools' not found in PATH. Cannot validate SBOM."; \
echo " Install: pip install spdx-tools"; \
echo ""; \
exit 1; \
fi
@rm -rf $(abs_builddir)/_sbom_staging
@set -e; \
trap 'rm -rf $(abs_builddir)/_sbom_staging' EXIT INT TERM HUP; \
if test -n "$(SBOM_LIB_OVERRIDE)"; then \
if test ! -f "$(SBOM_LIB_OVERRIDE)"; then \
echo ""; \
echo "ERROR: SBOM_LIB_OVERRIDE=$(SBOM_LIB_OVERRIDE) does not exist."; \
echo ""; \
exit 1; \
fi; \
sbom_lib="$(SBOM_LIB_OVERRIDE)"; \
else \
$(MAKE) install DESTDIR=$(abs_builddir)/_sbom_staging; \
sbom_lib=""; \
for lib in \
$(addprefix "$(abs_builddir)/_sbom_staging$(libdir)"/,$(WOLFSSL_LIB_DSO_BASENAMES)) \
"$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssl.dll \
"$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssl.dll.a \
"$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssl.lib \
"$(abs_builddir)/_sbom_staging$(libdir)"/wolfssl.lib \
"$(abs_builddir)/_sbom_staging$(libdir)"/libwolfssl.a; do \
if test -f "$$lib"; then sbom_lib="$$lib"; break; fi; \
done; \
if test -z "$$sbom_lib"; then \
echo ""; \
echo "ERROR: No installed wolfSSL library artifact found for SBOM."; \
echo " Searched in $(abs_builddir)/_sbom_staging$(libdir)"; \
echo " (configure with --enable-shared or --enable-static)"; \
echo ""; \
exit 1; \
fi; \
fi; \
echo "SBOM: hashing $$sbom_lib"; \
if test -z "$${SOURCE_DATE_EPOCH:-}" && test -n "$(GIT)" && \
$(GIT) -C "$(srcdir)" rev-parse --git-dir >/dev/null 2>&1; then \
sde=`$(GIT) -C "$(srcdir)" log -1 --format=%ct 2>/dev/null`; \
if test -n "$$sde"; then \
SOURCE_DATE_EPOCH="$$sde"; \
export SOURCE_DATE_EPOCH; \
fi; \
fi; \
$(PYTHON3) $(srcdir)/scripts/gen-sbom \
--name $(PACKAGE) \
--version $(PACKAGE_VERSION) \
--license-file $(srcdir)/LICENSING \
$(if $(SBOM_LICENSE_OVERRIDE),--license-override '$(SBOM_LICENSE_OVERRIDE)') \
$(if $(SBOM_LICENSE_TEXT),--license-text '$(SBOM_LICENSE_TEXT)') \
$(if $(SBOM_DOCUMENT_NAMESPACE),--document-namespace '$(SBOM_DOCUMENT_NAMESPACE)') \
--options-h $(abs_builddir)/wolfssl/options.h \
--lib "$$sbom_lib" \
--dep-libz $(ENABLED_LIBZ) \
--dep-liboqs $(ENABLED_LIBOQS) \
$(foreach dv,$(SBOM_DEP_VERSIONS),--dep-version '$(dv)') \
--cdx-out $(abs_builddir)/$(SBOM_CDX) \
--spdx-out $(abs_builddir)/$(SBOM_SPDX); \
$(PYSPDXTOOLS) --infile $(abs_builddir)/$(SBOM_SPDX) \
--outfile $(abs_builddir)/$(SBOM_SPDX_TV)
install-sbom: sbom
$(MKDIR_P) $(DESTDIR)$(sbomdir)
$(INSTALL_DATA) $(SBOM_CDX) $(DESTDIR)$(sbomdir)/
$(INSTALL_DATA) $(SBOM_SPDX) $(DESTDIR)$(sbomdir)/
$(INSTALL_DATA) $(SBOM_SPDX_TV) $(DESTDIR)$(sbomdir)/
uninstall-sbom:
-rm -f $(DESTDIR)$(sbomdir)/$(SBOM_CDX)
-rm -f $(DESTDIR)$(sbomdir)/$(SBOM_SPDX)
-rm -f $(DESTDIR)$(sbomdir)/$(SBOM_SPDX_TV)
CLEANFILES += $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV)
# Security advisory generation (CSAF 2.0 + CycloneDX 1.6 VEX)
#
# `make advisory` is a thin wrapper around scripts/gen-advisory: it feeds the
# script the canonical advisory single-source-of-truth under advisories/ and is
# byte-for-byte interchangeable with running the script by hand. Equivalent
# invocations:
#
# make advisory
# python3 scripts/gen-advisory # uses the same defaults
# python3 scripts/gen-advisory \
# --records-dir advisories/records \
# --vex-overlay advisories/vex-overlay.json \
# --out-dir advisories/out
#
# Inputs (tracked in git): advisories/records/*.json + advisories/vex-overlay.json
# Outputs (build artifacts): advisories/out/*.{csaf,cdx}.json
ADVISORY_RECORDS_DIR = $(srcdir)/advisories/records
ADVISORY_OVERLAY = $(srcdir)/advisories/vex-overlay.json
ADVISORY_OUT_DIR = $(abs_builddir)/advisories/out
advisorydir = $(datadir)/doc/$(PACKAGE)/advisories
.PHONY: advisory install-advisory uninstall-advisory
# Generate one CSAF + one CycloneDX VEX document per CVE record. Honors
# SOURCE_DATE_EPOCH for reproducible output (set from the last git commit when
# unset and a git tree is available), exactly like `make sbom`.
advisory:
@if test -z "$(PYTHON3)"; then \
echo ""; \
echo "ERROR: 'python3' not found in PATH. Cannot generate advisories."; \
echo ""; \
exit 1; \
fi
@set -e; \
if test -z "$${SOURCE_DATE_EPOCH:-}" && test -n "$(GIT)" && \
$(GIT) -C "$(srcdir)" rev-parse --git-dir >/dev/null 2>&1; then \
sde=`$(GIT) -C "$(srcdir)" log -1 --format=%ct 2>/dev/null`; \
if test -n "$$sde"; then \
SOURCE_DATE_EPOCH="$$sde"; \
export SOURCE_DATE_EPOCH; \
fi; \
fi; \
$(PYTHON3) $(srcdir)/scripts/gen-advisory \
--records-dir $(ADVISORY_RECORDS_DIR) \
--vex-overlay $(ADVISORY_OVERLAY) \
--out-dir $(ADVISORY_OUT_DIR)
install-advisory: advisory
$(MKDIR_P) $(DESTDIR)$(advisorydir)
@for f in $(ADVISORY_OUT_DIR)/*.json; do \
test -f "$$f" || continue; \
echo " $(INSTALL_DATA) $$f $(DESTDIR)$(advisorydir)/"; \
$(INSTALL_DATA) "$$f" $(DESTDIR)$(advisorydir)/; \
done
uninstall-advisory:
-rm -f $(DESTDIR)$(advisorydir)/*.csaf.json
-rm -f $(DESTDIR)$(advisorydir)/*.cdx.json
CLEANFILES += advisories/out/*.csaf.json advisories/out/*.cdx.json
# Ship the advisory generator inputs in the dist tarball so a downstream
# consumer can `./configure && make advisory` from a release. The per-CVE
# records are copied via dist-hook (glob) rather than listed in EXTRA_DIST so
# a newly-added record ships automatically: a hardcoded list silently drops
# new records from `make dist`, and the omission only surfaces as a failing
# downstream `make advisory`.
EXTRA_DIST += advisories/vex-overlay.json
dist-hook:
$(MKDIR_P) $(distdir)/advisories/records
@for f in $(srcdir)/advisories/records/*.json; do \
test -f "$$f" || continue; \
cp -p "$$f" $(distdir)/advisories/records/; \
done
# Bomsh (OmniBOR build artifact tracing + SBOM enrichment)
BOMSH_RAWLOG_BASE = $(abs_builddir)/bomsh_raw_logfile
BOMSH_RAWLOG = $(BOMSH_RAWLOG_BASE).sha1
BOMSH_CONF = $(abs_builddir)/_bomsh.conf
BOMSH_OMNIBORDIR = $(abs_builddir)/omnibor
BOMSH_SPDX_OUT = omnibor.wolfssl-$(PACKAGE_VERSION).spdx.json
bomshdir = $(datadir)/doc/$(PACKAGE)
.PHONY: bomsh install-bomsh uninstall-bomsh
# Self-contained: the traced rebuild also regenerates the SBOM, so users
# can run `make bomsh` directly without first running `make sbom`. This is
# also what makes the combined workflow correct: `make sbom` writes the SPDX,
# but `make bomsh` issues `make clean` (which removes it via CLEANFILES), so
# the only reliable way to enrich is to regenerate after the traced build.
#
# After the traced rebuild we discover the bomsh-traced library in
# $(abs_builddir)/src/.libs/ and pass it to the nested `make sbom` call as
# SBOM_LIB_OVERRIDE. Without the override `make sbom` would `make install
# DESTDIR=...` into a private tree, which triggers a libtool relink and
# produces a binary whose SHA-256 differs from the one bomtrace3 traced.
# That left the gitoid in `externalRefs` (which IS for the traced binary)
# and the SHA-256 in `checksums[]` (which was NOT) describing two different
# files in the same SPDX document. With the override they describe the
# same artefact, which is the invariant any auditor reading the document
# expects.
bomsh:
@if test -z "$(BOMTRACE3)"; then \
echo ""; \
echo "ERROR: 'bomtrace3' not found in PATH. Cannot generate OmniBOR data."; \
echo " Build bomtrace3 from: https://github.com/omnibor/bomsh"; \
echo ""; \
exit 1; \
fi
@if test -z "$(BOMSH_CREATE_BOM)"; then \
echo ""; \
echo "ERROR: 'bomsh_create_bom.py' not found in PATH. Cannot process OmniBOR data."; \
echo " Install from: https://github.com/omnibor/bomsh"; \
echo ""; \
exit 1; \
fi
$(MAKE) clean
@printf 'raw_logfile=%s\n' '$(BOMSH_RAWLOG_BASE)' > '$(BOMSH_CONF)'
$(BOMTRACE3) -c '$(BOMSH_CONF)' $(MAKE)
$(BOMSH_CREATE_BOM) -r '$(BOMSH_RAWLOG)' -b '$(BOMSH_OMNIBORDIR)'
@set -e; \
bomsh_artifact=""; \
for lib in \
$(addprefix "$(abs_builddir)/src/.libs"/,$(WOLFSSL_LIB_DSO_BASENAMES)) \
"$(abs_builddir)/src/.libs/libwolfssl.a" \
"$(abs_builddir)/src/libwolfssl.a"; do \
if test -f "$$lib"; then bomsh_artifact="$$lib"; break; fi; \
done; \
if test -z "$$bomsh_artifact"; then \
echo "NOTE: no built libwolfssl artifact found in $(abs_builddir)/src/.libs/"; \
echo " OmniBOR graph produced; SBOM regeneration + SPDX"; \
echo " enrichment skipped."; \
exit 0; \
fi; \
echo "bomsh: traced binary -> $$bomsh_artifact"; \
$(MAKE) sbom SBOM_LIB_OVERRIDE="$$bomsh_artifact"; \
if test -z "$(BOMSH_SBOM)"; then \
echo "NOTE: bomsh_sbom.py not in PATH; skipping SPDX enrichment."; \
echo " The OmniBOR graph in $(BOMSH_OMNIBORDIR) is still produced."; \
echo " The base SBOM in $(SBOM_SPDX) already hashes the bomsh-traced binary."; \
exit 0; \
fi; \
echo "Enriching SPDX with OmniBOR ExternalRefs (artifact: $$bomsh_artifact)..."; \
$(BOMSH_SBOM) \
-b '$(BOMSH_OMNIBORDIR)' \
-i '$(abs_builddir)/$(SBOM_SPDX)' \
-f "$$bomsh_artifact" \
-s spdx-json \
-O '$(abs_builddir)'
install-bomsh: bomsh
$(MKDIR_P) '$(DESTDIR)$(bomshdir)/omnibor'
@if test -d '$(BOMSH_OMNIBORDIR)'; then \
cp -R '$(BOMSH_OMNIBORDIR)/.' '$(DESTDIR)$(bomshdir)/omnibor/'; \
fi
@if test -f '$(abs_builddir)/$(BOMSH_SPDX_OUT)'; then \
$(INSTALL_DATA) '$(abs_builddir)/$(BOMSH_SPDX_OUT)' '$(DESTDIR)$(bomshdir)/'; \
fi
uninstall-bomsh:
-rm -rf '$(DESTDIR)$(bomshdir)/omnibor'
-rm -f '$(DESTDIR)$(bomshdir)/$(BOMSH_SPDX_OUT)'
CLEANFILES += $(BOMSH_RAWLOG) $(BOMSH_RAWLOG_BASE).sha256 $(BOMSH_CONF) $(BOMSH_SPDX_OUT)
# Hook SBOM/Bomsh cleanup into `make uninstall` so packagers don't leave
# stale artefacts behind after install-sbom/install-bomsh. uninstall-sbom
# and uninstall-bomsh use `rm -f` / `rm -rf` so they are idempotent and
# safe whether or not those targets were ever run. Depending on them
# rather than duplicating their bodies keeps the cleanup paths in lock
# step with install-sbom/install-bomsh.
uninstall-hook: uninstall-sbom uninstall-bomsh uninstall-advisory
+34
View File
@@ -34,6 +34,40 @@ applications which have previously used the OpenSSL package. For a complete
feature list, see [Chapter 4](https://www.wolfssl.com/docs/wolfssl-manual/ch4/)
of the wolfSSL manual.
## SBOM / CRA Compliance
wolfSSL provides a Software Bill of Materials (SBOM) for EU Cyber Resilience
Act (CRA) compliance via two entry points:
- `python3 scripts/gen-sbom …` for embedded / RTOS / IDE-based builds
(Keil, IAR, STM32CubeIDE, ESP-IDF, Zephyr, plain CMake, custom Makefile)
configured through a hand-edited `user_settings.h`. No autotools required.
- `make sbom` for Linux server / Debian / RPM / Yocto / FIPS-Ready
builds that already use `./configure && make`.
Both produce SPDX 2.3 + CycloneDX 1.6 JSON intended to satisfy the
NTIA minimum elements. The `make sbom` path additionally runs
SPDX-spec validation via `pyspdxtools` and gates the build on it;
the standalone path validates only on demand (see `doc/SBOM.md`
§ 1.3). Neither path runs an NTIA-minimum-elements checker by
default. See `doc/SBOM.md` for per-toolchain recipes and the full
flag reference.
## OmniBOR / Bomsh
wolfSSL supports generating an OmniBOR artifact dependency graph via
`make bomsh`, providing cryptographic traceability from the installed
library back to every source file that produced it. See `doc/SBOM.md`
for details.
## Security advisories (CSAF / VEX)
wolfSSL can generate machine-readable security advisories via
`make advisory` (requires `python3`), emitting one CSAF 2.0 document
and one CycloneDX 1.6 VEX document per CVE into `advisories/out/`
(`*.csaf.json` and `*.cdx.json`) from the git-tracked records under
`advisories/`. See section 21 of `INSTALL` for details.
## Notes, Please Read
### Note 1
+122
View File
@@ -0,0 +1,122 @@
{
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"cveMetadata": {
"cveId": "CVE-2026-5501",
"assignerOrgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"state": "PUBLISHED",
"assignerShortName": "wolfSSL",
"dateReserved": "2026-04-03T15:46:09.302Z",
"datePublished": "2026-04-10T03:07:39.604Z",
"dateUpdated": "2026-04-22T13:59:28.514Z"
},
"containers": {
"cna": {
"providerMetadata": {
"orgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"shortName": "wolfSSL",
"dateUpdated": "2026-04-10T03:07:39.604Z"
},
"title": "Improper Certificate Signature Verification in X.509 Chain Validation Allows Forged Leaf Certificates",
"problemTypes": [
{
"descriptions": [
{
"lang": "en",
"cweId": "CWE-295",
"description": "CWE-295 Improper certificate validation",
"type": "CWE"
}
]
}
],
"affected": [
{
"vendor": "wolfSSL",
"product": "wolfSSL",
"modules": [
"wolfSSL_X509_verify_cert"
],
"programFiles": [
"src/x509_str.c"
],
"versions": [
{
"status": "affected",
"version": "0",
"lessThanOrEqual": "5.9.0",
"versionType": "semver"
}
],
"defaultStatus": "unaffected"
}
],
"descriptions": [
{
"lang": "en",
"value": "wolfSSL_X509_verify_cert in the OpenSSL compatibility layer accepts a certificate chain in which the leaf's signature is not checked, if the attacker supplies an untrusted intermediate with Basic Constraints `CA:FALSE` that is legitimately signed by a trusted root. An attacker who obtains any leaf certificate from a trusted CA (e.g. a free DV cert from Let's Encrypt) can forge a certificate for any subject name with any public key and arbitrary signature bytes, and the function returns `WOLFSSL_SUCCESS` / `X509_V_OK`. The native wolfSSL TLS handshake path (`ProcessPeerCerts`) is not susceptible and the issue is limited to applications using the OpenSSL compatibility API directly, which would include integrations of wolfSSL into nginx and haproxy.",
"supportingMedia": [
{
"type": "text/html",
"base64": false,
"value": "wolfSSL_X509_verify_cert in the OpenSSL compatibility layer accepts a certificate chain in which the leaf's signature is not checked, if the attacker supplies an untrusted intermediate with Basic Constraints `CA:FALSE` that is legitimately signed by a trusted root. An attacker who obtains any leaf certificate from a trusted CA (e.g. a free DV cert from Let's Encrypt) can forge a certificate for any subject name with any public key and arbitrary signature bytes, and the function returns `WOLFSSL_SUCCESS` / `X509_V_OK`. The native wolfSSL TLS handshake path (`ProcessPeerCerts`) is not susceptible and the issue is limited to applications using the OpenSSL compatibility API directly, which would include integrations of wolfSSL into nginx and haproxy."
}
]
}
],
"references": [
{
"url": "https://github.com/wolfSSL/wolfssl/pull/10102"
}
],
"metrics": [
{
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
],
"cvssV4_0": {
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"attackRequirements": "NONE",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"vulnConfidentialityImpact": "HIGH",
"subConfidentialityImpact": "NONE",
"vulnIntegrityImpact": "HIGH",
"subIntegrityImpact": "NONE",
"vulnAvailabilityImpact": "NONE",
"subAvailabilityImpact": "NONE",
"exploitMaturity": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"valueDensity": "NOT_DEFINED",
"vulnerabilityResponseEffort": "NOT_DEFINED",
"providerUrgency": "NOT_DEFINED",
"version": "4.0",
"baseSeverity": "CRITICAL",
"baseScore": 9.3,
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
}
}
],
"credits": [
{
"lang": "en",
"value": "Calif.io in collaboration with Claude and Anthropic Research",
"type": "finder"
}
],
"source": {
"discovery": "EXTERNAL"
},
"x_generator": {
"engine": "Vulnogram 1.0.1"
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
{
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"cveMetadata": {
"cveId": "CVE-2026-5778",
"assignerOrgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"state": "PUBLISHED",
"assignerShortName": "wolfSSL",
"dateReserved": "2026-04-08T08:25:15.400Z",
"datePublished": "2026-04-09T21:45:09.053Z",
"dateUpdated": "2026-04-10T13:53:29.181Z"
},
"containers": {
"cna": {
"providerMetadata": {
"orgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"shortName": "wolfSSL",
"dateUpdated": "2026-04-09T21:45:09.053Z"
},
"title": "Integer underflow leads to out-of-bounds access in sniffer ChaCha decrypt path.",
"problemTypes": [
{
"descriptions": [
{
"lang": "en",
"cweId": "CWE-191",
"description": "CWE-191 Integer underflow (wrap or wraparound)",
"type": "CWE"
}
]
}
],
"affected": [
{
"vendor": "wolfSSL",
"product": "wolfSSL",
"modules": [
"Packet sniffer"
],
"programFiles": [
"src/sniffer.c"
],
"versions": [
{
"status": "affected",
"version": "0",
"lessThanOrEqual": "5.9.0",
"versionType": "semver"
}
],
"defaultStatus": "unaffected"
}
],
"descriptions": [
{
"lang": "en",
"value": "Integer underflow in wolfSSL packet sniffer <= 5.9.0 allows an attacker to cause a program crash in the AEAD decryption path by injecting a TLS record shorter than the explicit IV plus authentication tag into traffic inspected by ssl_DecodePacket. The underflow wraps a 16-bit length to a large value that is passed to AEAD decryption routines, causing a large out-of-bounds read and crash. An unauthenticated attacker can trigger this remotely via malformed TLS Application Data records.",
"supportingMedia": [
{
"type": "text/html",
"base64": false,
"value": "Integer underflow in wolfSSL packet sniffer &lt;= 5.9.0 allows an attacker to cause a program crash in the AEAD decryption path by injecting a TLS record shorter than the explicit IV plus authentication tag into traffic inspected by ssl_DecodePacket. The underflow wraps a 16-bit length to a large value that is passed to AEAD decryption routines, causing a large out-of-bounds read and crash. An unauthenticated attacker can trigger this remotely via malformed TLS Application Data records."
}
]
}
],
"references": [
{
"url": "https://github.com/wolfSSL/wolfssl/pull/10125"
}
],
"metrics": [
{
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
],
"cvssV4_0": {
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"attackRequirements": "PRESENT",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"vulnConfidentialityImpact": "NONE",
"subConfidentialityImpact": "NONE",
"vulnIntegrityImpact": "NONE",
"subIntegrityImpact": "NONE",
"vulnAvailabilityImpact": "HIGH",
"subAvailabilityImpact": "NONE",
"exploitMaturity": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"valueDensity": "NOT_DEFINED",
"vulnerabilityResponseEffort": "NOT_DEFINED",
"providerUrgency": "NOT_DEFINED",
"version": "4.0",
"baseSeverity": "HIGH",
"baseScore": 8.2,
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"
}
}
],
"credits": [
{
"lang": "en",
"value": "Zou Dikai",
"type": "finder"
}
],
"source": {
"discovery": "EXTERNAL"
},
"x_generator": {
"engine": "Vulnogram 1.0.1"
}
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"_comment": "Canonical wolfSSL VEX overlay consumed by `make advisory` / `scripts/gen-advisory`. Keyed by CVE id; carries the determinations the CVE Program record cannot express (analysis state, justification, fixed versions, remediation, optional FIPS product, optional build-reachability hedge). Constrained by scripts/advisory-vex-overlay.schema.json. To model a wolfCrypt FIPS module as a separate product, add a \"fips\" block per the format in scripts/advisory-vex-overlay.example.json using the real validated module version and CMVP certificate number (do NOT copy the illustrative placeholder values from the example).",
"CVE-2026-5501": {
"state": "exploitable",
"response": ["update"],
"detail": "Limited to applications using the OpenSSL compatibility API directly (wolfSSL_X509_verify_cert), such as nginx and haproxy integrations. The native wolfSSL TLS handshake path (ProcessPeerCerts) is not susceptible.",
"fixed_versions": ["5.9.1"],
"remediation": "Update to wolfSSL 5.9.1 or later, or avoid relying on wolfSSL_X509_verify_cert in the OpenSSL compatibility layer for chain validation."
},
"CVE-2026-5778": {
"state": "exploitable",
"response": ["update"],
"detail": "Integer underflow in the ChaCha20-Poly1305 decryption path of the packet sniffer.",
"requires_defines": ["WOLFSSL_SNIFFER", "HAVE_CHACHA", "HAVE_POLY1305"],
"default_status": "off",
"fixed_versions": ["5.9.1"],
"remediation": "Update to wolfSSL 5.9.1 or later. Builds without --enable-sniffer are not affected."
}
}
+12
View File
@@ -13178,6 +13178,18 @@ AC_SUBST([WOLFSSL_PREFIX_ABS])
AC_SUBST([WOLFSSL_LIBDIR_ABS])
AC_SUBST([WOLFSSL_INCLUDEDIR_ABS])
# SBOM generation
AC_PATH_PROG([PYTHON3], [python3])
AC_PATH_PROG([PYSPDXTOOLS], [pyspdxtools])
AC_PATH_PROG([GIT], [git])
AC_SUBST([ENABLED_LIBZ])
AC_SUBST([ENABLED_LIBOQS])
# Bomsh (OmniBOR build artifact tracing + SBOM enrichment)
AC_PATH_PROG([BOMTRACE3], [bomtrace3])
AC_PATH_PROG([BOMSH_CREATE_BOM], [bomsh_create_bom.py])
AC_PATH_PROG([BOMSH_SBOM], [bomsh_sbom.py])
# FINAL
AC_CONFIG_FILES([stamp-h], [echo timestamp > stamp-h])
AC_CONFIG_FILES([Makefile
+339
View File
@@ -0,0 +1,339 @@
# wolfSSL and the EU Cyber Resilience Act
This guide is for product teams that ship a product containing wolfSSL and
need to satisfy EU Cyber Resilience Act (CRA) obligations related to software
component transparency and build traceability.
## Background
The CRA requires manufacturers of products with digital elements placed on
the EU market to identify and document the software components in those
products. The practical requirement is a machine-readable Software Bill of
Materials (SBOM) covering all open-source and third-party components,
following the NTIA minimum element guidelines.
wolfSSL provides two complementary artefacts to help you meet this
requirement:
| Artefact | Produced by | What it answers |
|---|---|---|
| SBOM (SPDX 2.3 + CycloneDX 1.6) | `make sbom` | *What* is in wolfSSL (identity, license, CPE, PURL, checksum) |
| OmniBOR artifact graph | `make bomsh` | *How* wolfSSL was built (cryptographic source-to-binary traceability) |
For most CRA use cases the SBOM alone is sufficient. The OmniBOR graph
provides a deeper audit trail if your compliance posture requires it.
## Quick Start
wolfSSL exposes two SBOM entry points, depending on how you build the
library.
### Embedded / RTOS / IDE-based builds (no `./configure`)
If your product builds wolfSSL with a hand-edited `user_settings.h` and a
custom Makefile, Keil / IAR / STM32CubeIDE project, ESP-IDF / Zephyr
component, or plain CMake, invoke `scripts/gen-sbom` directly. No
autotools required:
```sh
python3 wolfssl/scripts/gen-sbom \
--name wolfssl --version 5.9.1 \
--license-file wolfssl/LICENSING \
--user-settings wolfssl/wolfssl/wolfcrypt/settings.h \
--user-settings-include wolfssl \
--user-settings-include path/to/your/user_settings_dir \
--user-settings-define WOLFSSL_USER_SETTINGS \
--srcs wolfssl/wolfcrypt/src/aes.c [and the rest of your wolfssl source list] \
--cdx-out wolfssl-5.9.1.cdx.json \
--spdx-out wolfssl-5.9.1.spdx.json
```
Requires Python 3 + `pip install pcpp` (used to walk
`user_settings.h` the same way the C compiler does).
See `doc/SBOM.md` § 1 for per-toolchain recipes (Keil, IAR,
STM32CubeIDE, ESP-IDF, Zephyr, plain CMake) and the full flag reference.
### Linux / autotools builds
For Debian / RPM / Yocto / FIPS-Ready / cloud builds that already use
`./configure && make`:
```sh
./configure
make
make sbom # produces wolfssl-<version>.spdx.json, .cdx.json, .spdx
make bomsh # optional: produces omnibor/ + OmniBOR-enriched SPDX
```
`make sbom` is a convenience wrapper around the same `scripts/gen-sbom`
script the embedded path uses.
See `doc/SBOM.md` for prerequisites and full details on both entry
points.
## What wolfSSL Provides
After `make sbom`:
```
wolfssl-<version>.spdx.json SPDX 2.3 JSON (machine processing)
wolfssl-<version>.cdx.json CycloneDX 1.6 JSON (supply-chain tooling, VEX)
wolfssl-<version>.spdx SPDX 2.3 tag-value (human review, archival)
```
After `make bomsh` (with `make sbom` already run):
```
omnibor/ OmniBOR artifact dependency graph
omnibor.wolfssl-<version>.spdx.json SPDX enriched with PERSISTENT-ID gitoid
```
## Integrating wolfSSL into Your Product SBOM
Your product SBOM needs to list wolfSSL as a component. The two standard
approaches are to reference wolfSSL's SBOM document from yours, or to copy
the wolfSSL package entry directly into your document.
### SPDX: external document reference (recommended)
Reference wolfSSL's SPDX document from your product's SPDX document using
`externalDocumentRefs`. This keeps the documents separate and lets wolfSSL's
SBOM stand as an independently verifiable artefact.
wolfSSL ships the generated SBOM with the source distribution and does not
currently publish it at a fixed, resolvable URL. In the `spdxDocument`
field below, substitute the URI under which your distribution mirrors
`wolfssl-<version>.spdx.json` (e.g. an artifact server, OCI registry, or
distribution mirror you control). SPDX 2.3 §6.5 only requires the value
be a unique URI; if you do not re-host, the `urn:uuid:<uuid5-derived>`
form that `make sbom` emits by default is acceptable.
```json
{
"externalDocumentRefs": [
{
"externalDocumentId": "DocumentRef-wolfssl",
"spdxDocument": "<URI where your distribution serves wolfssl-<version>.spdx.json>",
"checksum": {
"algorithm": "SHA256",
"checksumValue": "<sha256-of-wolfssl-spdx.json>"
}
}
]
}
```
Then express the dependency in your `relationships` section:
```json
{
"spdxElementId": "SPDXRef-Package-YourProduct",
"relatedSpdxElement": "DocumentRef-wolfssl:SPDXRef-Package-wolfssl",
"relationshipType": "DYNAMIC_LINK"
}
```
Use `STATIC_LINK` if you link wolfSSL statically, `DYNAMIC_LINK` if you
use the shared library, or `CONTAINS` if you redistribute the source.
Alternatively, copy the wolfSSL package entry from its SPDX document
directly into your own SPDX document and add the `DYNAMIC_LINK` /
`STATIC_LINK` relationship to your product package.
### CycloneDX: component reference
Include wolfSSL as a component in your CycloneDX BOM, referencing the
wolfSSL CycloneDX document via an external reference of type `bom`.
As with the SPDX `spdxDocument` URI above, wolfSSL does not currently
publish CycloneDX SBOMs at a fixed, resolvable URL; substitute the URI
under which your distribution mirrors `wolfssl-<version>.cdx.json`.
```json
{
"type": "library",
"name": "wolfssl",
"version": "<version>",
"purl": "pkg:github/wolfSSL/wolfssl@v<version>",
"cpe": "cpe:2.3:a:wolfssl:wolfssl:<version>:*:*:*:*:*:*:*",
"licenses": [{ "license": { "id": "GPL-3.0-only" } }],
"externalReferences": [
{
"type": "bom",
"url": "<URI where your distribution serves wolfssl-<version>.cdx.json>",
"hashes": [
{
"alg": "SHA-256",
"content": "<sha256-of-wolfssl-cdx.json>"
}
]
}
]
}
```
## Commercial License Users
wolfSSL's published SBOM records `licenseConcluded: GPL-3.0-only`, which
reflects the open-source license. If you are distributing a product under a
wolfSSL commercial license, you have two options:
### Option 1: regenerate the SBOM with your license expression
Pass `SBOM_LICENSE_OVERRIDE` to `make sbom` to bake your SPDX expression
directly into the artefact (preferred — survives re-runs, no manual editing):
```sh
make sbom \
SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial \
SBOM_LICENSE_TEXT=/path/to/wolfssl-commercial-license.txt
```
`SBOM_LICENSE_TEXT` is **required** whenever `SBOM_LICENSE_OVERRIDE` uses a
custom `LicenseRef-*` identifier. SPDX 2.3 §10.1 requires the actual licence
text to be embedded in `hasExtractedLicensingInfos` for any LicenseRef used in
the document; SPDX-conformant validators (e.g. `pyspdxtools`) will reject the
SBOM otherwise. `ntia-conformance-checker` validates a separate set of NTIA
minimum elements (supplier, component name, version, unique identifier,
dependency relationships, author, timestamp) and will **not** catch a missing
extracted-text block — do not rely on it to gate this case. The file should
contain the plain-text licence agreement you received from wolfSSL.
If `SBOM_LICENSE_OVERRIDE` is set to a `LicenseRef-*` and `SBOM_LICENSE_TEXT`
is missing, `make sbom` exits with an error rather than emit an invalid SBOM
that might end up in front of a regulator.
For a stock SPDX-listed identifier (`Apache-2.0`, `MIT`, etc.) the
`SBOM_LICENSE_TEXT` argument is unnecessary because validators already know
the canonical text.
Or invoke the generator directly with `--license-override` /
`--license-text` if you are producing the SBOM outside the standard make
target.
### Option 2: update your product SBOM's reference to wolfSSL
Leave the upstream SBOM file alone and override `licenseConcluded` on the
wolfSSL package entry in *your* product SBOM:
```json
"licenseConcluded": "LicenseRef-wolfSSL-Commercial"
```
Do not modify the wolfSSL-published SBOM file in place; either regenerate it
with the override (Option 1) or override at the consumer level (Option 2).
## Reproducible SBOMs
The generator honors `SOURCE_DATE_EPOCH` for the SBOM creation timestamp and
uses deterministic UUIDs derived from the package name and version, so two
runs of `make sbom` against the same source tree, library binary, and build
options produce byte-identical `.spdx.json` and `.cdx.json` files. This
matters for downstream attestation pipelines that hash SBOMs as part of a
provenance chain.
`make sbom` will derive `SOURCE_DATE_EPOCH` from `git log -1 --format=%ct` if
you do not set it explicitly and the wolfSSL source tree is a git checkout.
## Build Provenance (OmniBOR)
The CRA also encourages transparency about *how* software is built, not just
*what* it contains. Running `make bomsh` after `make sbom` produces an
OmniBOR artifact dependency graph and an enriched SPDX document:
```
omnibor.wolfssl-<version>.spdx.json
```
This file is identical to `wolfssl-<version>.spdx.json` except it adds a
`PERSISTENT-ID gitoid` entry to the wolfSSL package's `externalRefs`:
```json
{
"referenceCategory": "PERSISTENT-ID",
"referenceType": "gitoid",
"referenceLocator": "gitoid:blob:sha1:<hash>"
}
```
The `gitoid` is the entry point into the OmniBOR Merkle DAG stored in
`omnibor/`. A CRA auditor or supply-chain tool can follow that identifier
through the graph to verify that a specific `libwolfssl.so` binary was
produced from a specific, unmodified set of source files.
Use `omnibor.wolfssl-<version>.spdx.json` in place of the plain SPDX file
when you want to include this traceability claim in your product SBOM.
## What to Give Your Auditor
For a CRA conformity assessment, provide:
| File | Purpose |
|---|---|
| `wolfssl-<version>.spdx.json` | Machine-readable component identity (SPDX 2.3) |
| `wolfssl-<version>.cdx.json` | Machine-readable component identity (CycloneDX 1.6) |
| `wolfssl-<version>.spdx` | Human-readable tag-value form |
| `omnibor/` + `omnibor.wolfssl-<version>.spdx.json` | Build traceability (optional, if bomsh was run) |
If you have a product-level SBOM that references wolfSSL via
`ExternalDocumentRef` (SPDX) or a `bom` external reference (CycloneDX),
include that product SBOM alongside the wolfSSL artefacts.
CRA Article 10 also obliges manufacturers to handle and disclose
vulnerabilities. wolfSSL's vulnerability disclosure process is documented
in [`SECURITY-POLICY.md`](../SECURITY-POLICY.md) at the repository root,
with a mandatory reporting template at
[`SECURITY-REPORT-TEMPLATE.md`](../SECURITY-REPORT-TEMPLATE.md). Reports
go to **support@wolfssl.com**; published advisories live at
<https://www.wolfssl.com/docs/security-vulnerabilities/>. The policy
explicitly accommodates ecosystem-coordination embargoes for downstream
integrators and certification bodies — point your auditor at this so
they can verify the disclosure path before signing off.
## Further Reading
### wolfSSL documentation
- `doc/SBOM.md` — unified reference covering SBOM generation, OmniBOR/Bomsh
build provenance, combined workflow, output formats, and implementation notes
- [`SECURITY-POLICY.md`](../SECURITY-POLICY.md) — vulnerability disclosure
policy, severity rubric, coordinated-disclosure practice, embargo
extensions for downstream integrators and certification bodies
- [`SECURITY-REPORT-TEMPLATE.md`](../SECURITY-REPORT-TEMPLATE.md) —
mandatory template for vulnerability reports submitted to wolfSSL
### OpenSSF guidance
- [CRA Brief Guide for OSS Developers](https://best.openssf.org/CRA-Brief-Guide-for-OSS-Developers.html)
— Clarifies when the CRA applies to open source projects and
maintainers, and what obligations fall on manufacturers integrating
OSS components into commercial products (i.e., you, if you ship a
product containing wolfSSL).
- [SBOM in Compliance](https://sbom-catalog.openssf.org/sbom-compliance.html)
— OpenSSF SBOM Everywhere SIG survey of the global regulatory
landscape: CRA, NTIA minimum elements, US EO 14028, Germany TR-03183,
and others. Useful for understanding how wolfSSL's SBOM artefacts map
to each framework.
- [Getting Started with SBOMs](https://sbom-catalog.openssf.org/getting-started)
— OpenSSF SBOM Everywhere SIG guidance on SBOM generation approaches
(build-integrated vs. separate tooling), phase selection, and
publication. wolfSSL's `make sbom` follows the build-integrated
approach recommended here.
- [OpenSSF CRA Policy Hub](https://openssf.org/category/policy/cra/)
— Ongoing OpenSSF coverage of CRA developments, implementation
guidance, and community responses.
- [SBOM Everywhere Wiki](https://sbom-catalog.openssf.org/)
— OpenSSF SIG home: tooling catalog, working group resources, naming
conventions, and cross-format guidance for SPDX and CycloneDX.
### Standards
- SPDX 2.3 specification: https://spdx.github.io/spdx-spec/v2.3/
- CycloneDX 1.6 specification: https://cyclonedx.org/specification/overview/
- NTIA minimum elements for an SBOM:
https://www.ntia.gov/report/2021/minimum-elements-software-bill-materials-sbom
+778
View File
@@ -0,0 +1,778 @@
# wolfSSL SBOM and Build Provenance
wolfSSL provides two complementary artefacts for software supply chain
transparency:
| Artefact | Target | Answers |
|---|---|---|
| SBOM (SPDX 2.3 + CycloneDX 1.6) | `scripts/gen-sbom` / `make sbom` | *What* wolfSSL is: component identity, license, checksums, CPE, PURL |
| OmniBOR artifact graph | `make bomsh` | *How* wolfSSL was built: cryptographic source-to-binary traceability |
Together they provide full coverage for the EU Cyber Resilience Act (CRA)
and similar supply chain transparency requirements. Each target is
independently useful; running both produces an enriched SPDX document that
bridges the two artefacts with a single `PERSISTENT-ID gitoid` reference.
The SBOM generator has two entry points so both customer segments are
covered:
| Entry point | Who it is for | Build system |
|---|---|---|
| `python3 scripts/gen-sbom …` (standalone) | Embedded / RTOS customers building with their own Makefile, Keil, IAR, STM32CubeIDE, ESP-IDF, Zephyr, plain CMake, etc. | Any |
| `make sbom` (autotools wrapper) | Linux server / Debian / RPM / Yocto / FIPS-Ready customers running `./configure && make` | Autotools |
Both call the same Python core and produce SBOMs that pass SPDX 2.3
(`pyspdxtools`) and CycloneDX 1.6 (`cyclonedx-bom` strict JSON validator)
schema validation. The autotools `make sbom` integration job
additionally runs `ntia-conformance-checker` against NTIA Minimum
Elements 2021; see `.github/workflows/sbom.yml` for the exact set of
validators run on every PR. Pick whichever matches your build flow.
---
## 1. Standalone Python tool (recommended for embedded / IDE builds)
`scripts/gen-sbom` is pure Python 3 stdlib (plus an optional `pcpp` dep,
see below). Customers who configure wolfSSL via a hand-edited
`user_settings.h` and link wolfSSL source files directly into firmware
invoke it directly, without running `./configure` or producing a
standalone `libwolfssl.a`.
### 1.1 Quick start
```sh
python3 scripts/gen-sbom \
--name wolfssl --version 5.9.1 \
--license-file LICENSING \
--user-settings wolfssl/wolfcrypt/settings.h \
--user-settings-include . \
--user-settings-include path/to/your/user_settings_dir \
--user-settings-define WOLFSSL_USER_SETTINGS \
--srcs wolfcrypt/src/aes.c wolfcrypt/src/sha.c \
wolfcrypt/src/sha256.c wolfcrypt/src/dh.c \
wolfcrypt/src/random.c \
--cdx-out wolfssl-5.9.1.cdx.json \
--spdx-out wolfssl-5.9.1.spdx.json
```
That command produces the same two SBOM JSON files (CycloneDX 1.6 and SPDX
2.3) that `make sbom` produces, with no autotools involvement.
### 1.2 What you provide
| Flag | What | Where it comes from |
|---|---|---|
| `--name wolfssl` | Component name | Hard-coded; always `wolfssl` |
| `--version 5.9.1` | Component version | Whatever wolfSSL release you pulled |
| `--license-file LICENSING` | wolfSSL's `LICENSING` file | Already in your wolfSSL source tree |
| `--user-settings wolfssl/wolfcrypt/settings.h` | wolfSSL's master settings header | Already in your wolfSSL source tree |
| `--user-settings-include DIR` (repeatable) | Include path containing your `user_settings.h` and the wolfSSL tree | Same as the `-I` flags in your build |
| `--user-settings-define NAME[=VALUE]` (repeatable) | Macros to predefine for preprocessing | Same as the `-D` flags in your build (at minimum: `-D WOLFSSL_USER_SETTINGS`) |
| `--srcs PATH …` | wolfSSL source files compiled into your firmware | The same source list you pass to your compiler |
| `--srcs-file PATH` | A file listing the wolfSSL source files, one per line (`#` comments and blank lines ignored) | Emitted mechanically by your IDE / build (link map, project export) when the list is too long for the command line |
| `--cdx-out / --spdx-out` | Output paths for the SBOM JSON files | Anywhere you want |
Exactly one component-checksum source is required: `--lib`, `--srcs` /
`--srcs-file`, or `--no-artifact-hash` (see § 1.4).
Optional flags:
| Flag | When to use it |
|---|---|
| `--supplier "Acme Inc."` | Override the default `wolfSSL Inc.` (rare) |
| `--dep-libz yes` | If your build links `libz` |
| `--dep-liboqs yes` | If your build links `liboqs` |
| `--dep-version libz=1.3.1` | Explicit dep version when `pkg-config` is unavailable (typical cross-compile) |
| `--license-override LicenseRef-wolfSSL-Commercial` | If you are a commercial licensee, not GPL |
| `--license-text /path/to/commercial-license.txt` | Required when `--license-override` is a `LicenseRef-*` |
| `--no-artifact-hash` | Record a placeholder checksum when **no** hashable artefact exists (ROM image, HSM firmware, binary-only redistribution). Mutually exclusive with `--lib` / `--srcs` / `--srcs-file` (see § 1.4) |
| `--document-namespace https://example.com/sbom/wolfssl-5.9.1.spdx.json` | Override the SPDX `documentNamespace`. Default is a deterministic `urn:uuid:` derived from `--name`/`--version` (SPDX 2.3 §6.5 requires only uniqueness, not resolvability). Set this when **your** distribution re-hosts the SBOM under a stable URL. |
### 1.3 Dependencies
- **Python 3**. Required. Stdlib only when using `--options-h`.
- **`pcpp`** (`pip install pcpp`). Required only when using
`--user-settings`. pcpp is a pure-Python C preprocessor that walks
`settings.h` and your `user_settings.h` the same way the C compiler
does, so the SBOM build properties reflect the actual compiled
configuration rather than just the literal text of `user_settings.h`.
If pcpp is not available you can pre-process externally with the
compiler and pass the result via `--options-h` (see § 1.5).
- **`pyspdxtools`** (`pip install spdx-tools`). Optional. Needed only
if you want to validate the produced SPDX or convert it to tag-value
form.
### 1.4 What the SBOM checksum represents
In a `make sbom` build the `hashes` / `checksums` field in the SBOM is
the SHA-256 of `libwolfssl.so` / `libwolfssl.a` / `libwolfssl.dylib`.
In an embedded build there is typically no separate library archive —
wolfSSL `.c` files are compiled directly into your firmware. Asking the
customer to synthesize a `.a` purely for SBOM purposes would be artificial
and would make the build harder. Instead, the standalone path computes
an OmniBOR-compatible Merkle hash over the wolfSSL source files you list
in `--srcs`:
1. For each source file, compute its OmniBOR `gitoid` (SHA-256 over
`"blob <size>\0" + filecontents`, byte-identical to
`git hash-object --object-format=sha256`).
2. Sort by basename.
3. Hash the concatenated `(basename, gitoid)` pairs.
The resulting hash:
- represents *"the wolfSSL source code that is in this firmware"*, which
is what an auditor actually wants to see;
- is independent of the order you pass `--srcs`, the absolute paths on
the build host, or the build host's filesystem;
- changes if any compiled-in source byte changes (catches tampering and
back-ported patches);
- interoperates with bomsh / OmniBOR tooling, which key off the same
gitoid format.
Each standalone SBOM is annotated with extra properties so the
checksum's semantics are unambiguous to downstream consumers:
```json
{ "name": "wolfssl:sbom:hash-kind", "value": "source-merkle-omnibor" },
{ "name": "wolfssl:sbom:hash-source", "value": "srcs" },
{ "name": "wolfssl:sbom:source-set", "value": "aes.c,dh.c,sha.c,sha256.c,..." }
```
`wolfssl:sbom:hash-source` is the coarse, stable provenance tag that
downstream tooling filters on — which **input** the checksum came from:
| `hash-source` | Meaning |
|---|---|
| `lib` | SHA-256 of a built library archive (`--lib`) |
| `srcs` | OmniBOR gitoid Merkle hash of the compiled source set (`--srcs` / `--srcs-file`) |
| `none` | Placeholder; no hashable artefact was available (`--no-artifact-hash`) |
`wolfssl:sbom:hash-kind` carries the finer implementation detail
(`library-binary` vs `source-merkle-omnibor` vs `none`); `hash-source`
is what an integrator keys on without needing to know the hashing
internals.
#### No hashable artefact (`--no-artifact-hash`)
For ROM images, HSM firmware, or binary-only redistributions where
neither a library archive nor the compiled source files are accessible,
pass `--no-artifact-hash`. The checksum field is then a synthetic
64-zero placeholder (`0000…0000`), and the SBOM carries:
```json
{ "name": "wolfssl:sbom:hash-source", "value": "none" },
{ "name": "wolfssl:sbom:no-artifact-hash-note", "value": "No artefact hash was available …" }
```
The placeholder can never collide with a real SHA-256, and the note
directs integrators to contact wolfSSL for an integrity-verification
approach appropriate to their build. Use this only as a last resort:
a `srcs`-based hash is strongly preferred because it is independently
verifiable against the public wolfSSL source tree.
The autotools `make sbom` path keeps `wolfssl:sbom:hash-kind` implicit
(equal to `library-binary`) so its output stays byte-identical to
previous releases.
### 1.5 Pre-processed defines (no pcpp needed)
If `pcpp` is unavailable on your build host or you prefer to use the
compiler that actually builds wolfSSL, dump its post-preprocessor `#define`
table and pass that to `--options-h`:
```sh
$CC $CFLAGS -dM -E \
-DWOLFSSL_USER_SETTINGS \
-include wolfssl/wolfcrypt/settings.h \
-x c /dev/null > build/wolfssl-defines.h
python3 scripts/gen-sbom \
--name wolfssl --version 5.9.1 \
--license-file LICENSING \
--options-h build/wolfssl-defines.h \
--srcs wolfcrypt/src/aes.c wolfcrypt/src/sha.c ... \
--cdx-out wolfssl-5.9.1.cdx.json \
--spdx-out wolfssl-5.9.1.spdx.json
```
`--options-h` reads any flat C header containing `#define NAME VALUE`
lines, so the GCC / Clang / `armclang` `-dM -E` output drops in directly.
For IAR (`--predef_macros`) or legacy Keil `armcc` (`--list_macros`) the
output may need a one-line `sed` to match the `#define NAME VALUE`
shape.
**Noise filtering.** A raw `$CC -dM -E` dump contains hundreds of
compiler / preprocessor reserved identifiers (`__VERSION__`, `__SSE2__`,
`__INT_FAST32_MAX__`, `_LP64`, …) and on macOS also Apple's entire
`<TargetConditionals.h>` family (`TARGET_OS_MAC=1`, `TARGET_IPHONE_*`,
…) which leak in via system header inclusion. These describe the
*build host*, not wolfSSL, and would otherwise drown out the wolfSSL
configuration in the SBOM and break reproducibility across hosts.
`gen-sbom` filters them automatically; the SBOM ends up with only the
`HAVE_*` / `WOLFSSL_*` / `NO_*` / etc. macros that actually describe
the wolfSSL build. See `_is_noise_macro` in `scripts/gen-sbom` for the
exact policy and the test cases in `scripts/test_gen_sbom.py`
(`TestIsNoiseMacro`) for the pinned coverage.
Header-suffix carve-out: the filter also drops include-guard names
(`*_H` like `WOLF_CRYPT_SETTINGS_H`, `WOLFSSL_OPTIONS_H`) but
preserves real wolfSSL configuration that happens to end in `_H`.
The carve-out tokens are `HAVE_`, `NO_`, and `USE_`, which between
them cover every `_H`-suffixed configuration flag in the wolfSSL
tree:
* autoconf header probes — `HAVE_STDINT_H`, `WOLFSSL_HAVE_ATOMIC_H`,
`WOLFSSL_HAVE_ASSERT_H`, …
* stdlib disablement (NETOS / Telit / similar RTOS profiles) —
`NO_STDINT_H`, `NO_STDLIB_H`, `NO_LIMITS_H`, `NO_CTYPE_H`,
`NO_STRING_H`, `NO_STDDEF_H`, `WOLFSSL_NO_ASSERT_H`
* build-mode toggles — `USE_FLAT_TEST_H`, `USE_FLAT_BENCHMARK_H`
Customers using a `_H`-suffixed feature flag that does not carry
one of these tokens (e.g. a debug-only opt-in) should rename it
to drop the `_H` suffix, or open an issue to extend the carve-out.
The `--user-settings` (pcpp) path applies the same filter, so both
entry points produce semantically equivalent build-property sets for
the same effective configuration.
**Hard-fail on preprocessing errors.** When the `--user-settings`
path encounters an `#error` directive, an unbalanced `#if`, or a
missing `#include` while walking `settings.h`, `gen-sbom` exits
non-zero rather than emitting a partial SBOM. pcpp would otherwise
print a diagnostic and continue, producing an artefact that silently
omits whatever configuration came after the failure — exactly the
kind of silent drift a CRA reviewer cannot detect. Fix the upstream
issue (or supply the missing `--user-settings-include` /
`--user-settings-define` arguments) and rerun.
### 1.6 Per-IDE / per-toolchain recipes
#### 1.6.1 Custom Makefile (most embedded projects)
Drop these rules into your project Makefile. The `WOLFCRYPT_OBJS` /
`WOLFCRYPT_SRCS` variables almost certainly already exist in your build
since they list the wolfSSL files you compile.
```makefile
WOLFSSL_DIR ?= ../wolfssl
build/libwolfssl-sbom.a: $(WOLFCRYPT_OBJS)
$(AR) rcs $@ $^
sbom: build/libwolfssl-sbom.a
python3 $(WOLFSSL_DIR)/scripts/gen-sbom \
--name wolfssl --version 5.9.1 \
--license-file $(WOLFSSL_DIR)/LICENSING \
--user-settings $(WOLFSSL_DIR)/wolfssl/wolfcrypt/settings.h \
--user-settings-include $(WOLFSSL_DIR) \
--user-settings-include $(USER_SETTINGS_DIR) \
--user-settings-define WOLFSSL_USER_SETTINGS \
--srcs $(WOLFCRYPT_SRCS) \
--cdx-out wolfssl-5.9.1.cdx.json \
--spdx-out wolfssl-5.9.1.spdx.json
```
Note: the `.a` here is optional. If you prefer to skip it and rely on
`--srcs` for the checksum (the recommended embedded mode), drop the
`build/libwolfssl-sbom.a` rule and remove its dependency from `sbom:`.
#### 1.6.2 ESP-IDF (Espressif)
ESP-IDF builds with CMake/Ninja and exposes a `CMakeLists.txt` per
component. Add a custom target to `components/wolfssl/CMakeLists.txt`:
```cmake
add_custom_target(wolfssl-sbom
COMMAND python3 ${CMAKE_CURRENT_SOURCE_DIR}/scripts/gen-sbom
--name wolfssl --version 5.9.1
--license-file ${CMAKE_CURRENT_SOURCE_DIR}/LICENSING
--user-settings ${CMAKE_CURRENT_SOURCE_DIR}/wolfssl/wolfcrypt/settings.h
--user-settings-include ${CMAKE_CURRENT_SOURCE_DIR}
--user-settings-include ${WOLFSSL_USER_SETTINGS_DIR}
--user-settings-define WOLFSSL_USER_SETTINGS
--srcs ${WOLFSSL_SRCS}
--cdx-out ${CMAKE_BINARY_DIR}/wolfssl-5.9.1.cdx.json
--spdx-out ${CMAKE_BINARY_DIR}/wolfssl-5.9.1.spdx.json
VERBATIM)
```
Then `idf.py wolfssl-sbom` produces both SBOM files in the build
directory.
#### 1.6.3 Zephyr
```cmake
# In your application CMakeLists.txt or a Zephyr module CMake file:
add_custom_target(wolfssl-sbom
COMMAND ${PYTHON_EXECUTABLE} ${ZEPHYR_WOLFSSL_MODULE_DIR}/scripts/gen-sbom
--name wolfssl --version 5.9.1
--license-file ${ZEPHYR_WOLFSSL_MODULE_DIR}/LICENSING
--user-settings ${ZEPHYR_WOLFSSL_MODULE_DIR}/wolfssl/wolfcrypt/settings.h
--user-settings-include ${ZEPHYR_WOLFSSL_MODULE_DIR}
--user-settings-define WOLFSSL_USER_SETTINGS
--srcs ${WOLFSSL_SOURCES}
--cdx-out ${CMAKE_BINARY_DIR}/wolfssl.cdx.json
--spdx-out ${CMAKE_BINARY_DIR}/wolfssl.spdx.json)
```
Run with `west build -t wolfssl-sbom`.
#### 1.6.4 STM32CubeIDE
STM32CubeIDE generates Eclipse CDT-managed Makefiles. Add the SBOM
recipe as a *post-build step*: **Project → Properties → C/C++ Build →
Settings → Build Steps → Post-build steps**:
```sh
python3 ${ProjDirPath}/Drivers/wolfssl/scripts/gen-sbom \
--name wolfssl --version 5.9.1 \
--license-file ${ProjDirPath}/Drivers/wolfssl/LICENSING \
--user-settings ${ProjDirPath}/Drivers/wolfssl/wolfssl/wolfcrypt/settings.h \
--user-settings-include ${ProjDirPath}/Drivers/wolfssl \
--user-settings-include ${ProjDirPath}/Core/Inc \
--user-settings-define WOLFSSL_USER_SETTINGS \
--srcs ${ProjDirPath}/Drivers/wolfssl/wolfcrypt/src/aes.c [...] \
--cdx-out ${ProjDirPath}/wolfssl.cdx.json \
--spdx-out ${ProjDirPath}/wolfssl.spdx.json
```
#### 1.6.5 Keil μVision (MDK-ARM)
Use **Options for Target → User → Run #1 (After Build)**:
```
python3 .\Drivers\wolfssl\scripts\gen-sbom --name wolfssl --version 5.9.1 ^
--license-file .\Drivers\wolfssl\LICENSING ^
--user-settings .\Drivers\wolfssl\wolfssl\wolfcrypt\settings.h ^
--user-settings-include .\Drivers\wolfssl ^
--user-settings-define WOLFSSL_USER_SETTINGS ^
--srcs .\Drivers\wolfssl\wolfcrypt\src\aes.c [...] ^
--cdx-out .\wolfssl.cdx.json --spdx-out .\wolfssl.spdx.json
```
For legacy `armcc` 5.x toolchains where `-dM -E` is not available, use
the modern `armclang` (Keil v6) which is GCC-flag-compatible.
#### 1.6.6 IAR EWARM
Use **Project → Options → Build Actions → Post-build command line** (one
line, all on one logical line in EWARM):
```
python3 $PROJ_DIR$\..\wolfssl\scripts\gen-sbom --name wolfssl --version 5.9.1
--license-file $PROJ_DIR$\..\wolfssl\LICENSING
--user-settings $PROJ_DIR$\..\wolfssl\wolfssl\wolfcrypt\settings.h
--user-settings-include $PROJ_DIR$\..\wolfssl
--user-settings-define WOLFSSL_USER_SETTINGS
--srcs $PROJ_DIR$\..\wolfssl\wolfcrypt\src\aes.c [...]
--cdx-out $PROJ_DIR$\wolfssl.cdx.json
--spdx-out $PROJ_DIR$\wolfssl.spdx.json
```
#### 1.6.7 Plain CMake (any project)
```cmake
add_custom_target(wolfssl-sbom
COMMAND ${Python3_EXECUTABLE} ${CMAKE_SOURCE_DIR}/wolfssl/scripts/gen-sbom
--name wolfssl --version 5.9.1
--license-file ${CMAKE_SOURCE_DIR}/wolfssl/LICENSING
--user-settings ${CMAKE_SOURCE_DIR}/wolfssl/wolfssl/wolfcrypt/settings.h
--user-settings-include ${CMAKE_SOURCE_DIR}/wolfssl
--user-settings-include ${WOLFSSL_USER_SETTINGS_DIR}
--user-settings-define WOLFSSL_USER_SETTINGS
--srcs ${WOLFSSL_C_SOURCES}
--cdx-out ${CMAKE_BINARY_DIR}/wolfssl-${WOLFSSL_VERSION}.cdx.json
--spdx-out ${CMAKE_BINARY_DIR}/wolfssl-${WOLFSSL_VERSION}.spdx.json)
```
### 1.7 Reproducibility
The standalone path honors `SOURCE_DATE_EPOCH` exactly the same way
`make sbom` does. Two runs against the same source tree, settings, and
source set with the same `SOURCE_DATE_EPOCH` produce byte-identical
`.spdx.json` and `.cdx.json` files. This is regression-tested in CI.
---
## 2. Autotools convenience wrapper (`make sbom`)
For Linux server / Debian / RPM / Yocto / FIPS-Ready customers who
already run `./configure && make`, `make sbom` is a one-line shortcut
that wraps `scripts/gen-sbom` with all paths resolved automatically.
### 2.1 Quick start
```sh
./configure
make
make sbom
```
Requires `python3` and `pyspdxtools` (`pip install spdx-tools`).
### 2.2 Full coverage: component identity + build provenance
```sh
./configure
make
make sbom
make bomsh
```
Additionally requires `bomtrace3` and `bomsh_create_bom.py` in `PATH`.
See [Prerequisites for make bomsh](#31-prerequisites-for-make-bomsh) below.
All tools are detected by `configure`; either target fails with a clear
error message if a required tool is missing.
### 2.3 Output files
`make sbom` produces three files in the build directory:
| File | Format | Standard | Primary use |
|---|---|---|---|
| `wolfssl-<version>.cdx.json` | JSON | CycloneDX 1.6 | Supply-chain tooling, VEX |
| `wolfssl-<version>.spdx.json` | JSON | SPDX 2.3 | Machine processing |
| `wolfssl-<version>.spdx` | Tag-value | SPDX 2.3 | Human review, archival |
The `.spdx` tag-value file is produced by `pyspdxtools` converting the
`.spdx.json`. If the JSON fails SPDX validation, `make sbom` stops with
a non-zero exit and the tag-value file is not written.
### 2.4 SBOM contents
Both formats contain the same information:
| Field | Value |
|---|---|
| Name | `wolfssl` |
| Version | from `configure.ac` (`PACKAGE_VERSION`) |
| Type | library |
| Supplier | wolfSSL Inc. |
| License | detected from `LICENSING` file (currently `GPL-3.0-only`) |
| Copyright | `Copyright (C) 2006-<year> wolfSSL Inc.` |
| SHA-256 | hash of the installed `libwolfssl.so.X.Y.Z` |
| CPE | `cpe:2.3:a:wolfssl:wolfssl:<version>:*:*:*:*:*:*:*` |
| PURL | `pkg:github/wolfSSL/wolfssl@v<version>` (resolves directly in OSV / GHSA / Snyk / Trivy without per-vendor mapping) |
| Download location | `https://github.com/wolfSSL/wolfssl` |
| Third-party deps | none in a default build; `--with-libz` adds zlib and `--with-liboqs` adds liboqs (recorded as `DEPENDS_ON` packages with their own purl/CPE/supplier). All builds depend transitively on the host C runtime; this is not enumerated as an SBOM component since it is system-supplied and varies per runtime target. |
#### License detection
The license SPDX identifier is parsed from the `LICENSING` file at SBOM
generation time, not hardcoded. If the `LICENSING` file cannot be parsed,
`make sbom` warns and uses `NOASSERTION` rather than silently emitting a
wrong value.
#### Dual licensing
wolfSSL is available under `GPL-3.0-only` for open-source use, with a
commercial license for proprietary products. The default SBOM reflects the
open-source license. Commercial licensees should regenerate the SBOM with
`--license-override` set to their applicable SPDX expression — the generator
exposes this directly:
```sh
python3 scripts/gen-sbom \
--license-override LicenseRef-wolfSSL-Commercial \
--license-text /path/to/wolfssl-commercial-license.txt \
... other flags ...
```
`--license-text` is **required** whenever `--license-override` is a custom
`LicenseRef-*`: SPDX 2.3 mandates that any LicenseRef in `licenseConcluded`
or `licenseDeclared` be backed by a `hasExtractedLicensingInfos` entry that
embeds the actual licence text. Running without it is a configuration
error and the generator exits non-zero rather than emit a misleading SBOM
that auditors might then circulate.
For an SPDX-listed override (`Apache-2.0`, `MIT`, etc.), `--license-text`
is unnecessary because validators already know the canonical text.
`make sbom` plumbs both knobs through the matching make variables:
```sh
make sbom \
SBOM_LICENSE_OVERRIDE=LicenseRef-wolfSSL-Commercial \
SBOM_LICENSE_TEXT=/path/to/wolfssl-commercial-license.txt
```
#### Overriding the SPDX `documentNamespace`
By default the SPDX document's `documentNamespace` is a deterministic
`urn:uuid:<uuid5-derived>` value. SPDX 2.3 §6.5 only requires that this
field be a unique URI; it does **not** have to resolve to anything. The
default avoids asserting a hosted URL the wolfSSL project does not serve.
If your distribution re-publishes the SBOM under a stable URL you control,
set `SBOM_DOCUMENT_NAMESPACE` (or `--document-namespace` in the standalone
entry point) so downstream consumers can `externalDocumentRef` your
hosted copy:
```sh
make sbom \
SBOM_DOCUMENT_NAMESPACE=https://example.com/sbom/wolfssl-5.9.1.spdx.json
```
#### External dependency version detection
The optional external dependencies wolfSSL can link against (`libz` and
`liboqs`) are both installed packages and are queried via
`pkg-config --modversion` at SBOM generation time. The SBOM records each
linked library by its package name (`zlib`, `liboqs`) so that downstream
vulnerability scanners (OSV, Grype, Trivy, Dependency-Track) match CVEs
against the right component. Algorithm enablement (e.g. Falcon, which is
reachable only via liboqs) is captured separately as build properties
(`wolfssl:build:HAVE_FALCON` etc.) parsed from `wolfssl/options.h`.
If pkg-config does not report a version (the package is not installed, or
its `.pc` file is missing):
- SPDX records `versionInfo: NOASSERTION` and emits no `purl` external ref.
- CycloneDX omits the `version` and `purl` fields entirely and the generator
prints a warning to stderr.
For embedded / cross-compile builds without `pkg-config`, the standalone
entry point exposes a `--dep-version libz=1.3.1` override (see § 1.2). The
autotools path exposes the same override through the `SBOM_DEP_VERSIONS`
make variable (space-separated `KEY=VERSION` pairs), so a packaging host
that lacks the dependency's `.pc` file can still record the version instead
of `NOASSERTION`:
```sh
make sbom SBOM_DEP_VERSIONS='liboqs=0.10.0 libz=1.3.1'
```
### 2.5 Validating the SBOM manually
```sh
# Validate SPDX JSON
pyspdxtools --infile wolfssl-<version>.spdx.json
# Convert to another format (e.g. RDF)
pyspdxtools --infile wolfssl-<version>.spdx.json \
--outfile wolfssl-<version>.spdx.rdf
```
### 2.6 Installing the SBOM
```sh
make install-sbom # installs to $(datadir)/doc/wolfssl/
make uninstall-sbom # removes the installed files
```
The generated files are removed by `make clean`.
### 2.7 Implementation notes
SBOM generation is implemented in `scripts/gen-sbom` (Python 3, stdlib
only for the autotools path) and hooked into the autotools build via
`Makefile.am` and `configure.ac`. The script stages a `make install`
into a temporary directory, hashes the installed library, generates both
SBOM formats, then removes the staging directory. The `pyspdxtools`
validation and conversion step runs after generation and gates the build
on SPDX conformance.
The standalone embedded entry point (§ 1) calls the same script with
different flags; the autotools target is essentially a path-resolver
wrapper that finds the installed library, the autotools-generated
`options.h`, and the `pkg-config` versions of any linked deps.
---
## 3. make bomsh
`make bomsh` uses the [Bomsh](https://github.com/omnibor/bomsh) project to
trace the wolfSSL build under `bomtrace3` (a patched `strace`) and produce
an OmniBOR artifact dependency graph: a content-addressed Merkle DAG mapping
every built binary back to the exact set of source files that produced it.
### 3.1 Prerequisites for make bomsh
| Tool | Required | Where to get it |
|---|---|---|
| `bomtrace3` | yes | Build from source: [omnibor/bomsh](https://github.com/omnibor/bomsh) |
| `bomsh_create_bom.py` | yes | `scripts/` directory of the bomsh repo, placed in `PATH` |
| `bomsh_sbom.py` | no | Same; needed only for SPDX enrichment step |
`bomtrace3` is a patched `strace` — it is a userspace binary and requires no
kernel modifications. It uses the standard `ptrace()` syscall available on
any stock Linux kernel. The only environments where it may be unavailable
are containers running with a hardened seccomp profile or systems with
`kernel.yama.ptrace_scope=3`.
`make bomsh` is **Linux-host-only by design**. For non-Linux build hosts
(macOS, Windows), use a Linux CI runner / WSL2 / a Linux container. The
target running the produced wolfSSL binary can be anything — bomsh traces
the cross-compiler invocation on Linux regardless of what platform the
binary will eventually run on.
#### Building bomtrace3
```sh
git clone https://github.com/omnibor/bomsh
git clone https://github.com/strace/strace strace3
cd strace3
patch -p1 < ../bomsh/.devcontainer/patches/bomtrace3.patch
cp ../bomsh/.devcontainer/src/*.[hc] src/
./bootstrap && ./configure && make
cp src/strace ~/.local/bin/bomtrace3
```
Place `bomsh_create_bom.py` (and optionally `bomsh_sbom.py`) from the bomsh
`scripts/` directory somewhere in `PATH`.
### 3.2 What make bomsh does
1. Runs `make clean` to ensure a full rebuild. This is necessary because
`bomtrace3` intercepts syscalls live during compilation and cannot
post-process an already-built tree. This step also removes any prior
`wolfssl-<version>.{cdx,spdx}.json` from a stand-alone `make sbom`,
which is intentional: the document `make bomsh` enriches must come
from the *traced* rebuild, not from a stale pre-trace one.
2. Writes a build-local `_bomsh.conf` redirecting the raw logfile out of
`/tmp/` to the build directory (avoids collisions between concurrent
builds).
3. Runs `bomtrace3 -c _bomsh.conf make` — rebuilds wolfSSL under strace
tracing, recording every compiler invocation with its inputs and outputs.
4. Runs `bomsh_create_bom.py` to process the raw logfile and produce the
OmniBOR artifact graph in `omnibor/`.
5. Discovers the bomsh-traced library under `src/.libs/` and runs
`make sbom SBOM_LIB_OVERRIDE=<traced-library>` so the regenerated
SPDX hashes the same binary that bomsh traced (see § 3.5).
6. If `bomsh_sbom.py` is available, annotates the regenerated SPDX
document with OmniBOR `ExternalRef` identifiers, producing
`omnibor.wolfssl-<version>.spdx.json`.
### 3.3 Output files
| Path | Description |
|---|---|
| `omnibor/objects/` | OmniBOR artifact objects (SHA-1 content-addressed dependency graph) |
| `omnibor/metadata/bomsh/` | Bomsh build metadata |
| `omnibor.wolfssl-<ver>.spdx.json` | SPDX 2.3 JSON enriched with OmniBOR `ExternalRef` (produced only when both `bomsh_sbom.py` and `wolfssl-<ver>.spdx.json` are present) |
The `PERSISTENT-ID gitoid` entry added to the enriched SPDX looks like:
```json
{
"referenceCategory": "PERSISTENT-ID",
"referenceType": "gitoid",
"referenceLocator": "gitoid:blob:sha1:<hash>"
}
```
This sits alongside the existing CPE and PURL `externalRefs` on the wolfSSL
package entry and is the key into the OmniBOR Merkle DAG in `omnibor/`.
### 3.4 Installing
```sh
make install-bomsh # installs omnibor/ and enriched SPDX to $(datadir)/doc/wolfssl/
make uninstall-bomsh # removes installed files
```
The generated files are removed by `make clean`.
### 3.5 Implementation notes
`make bomsh` runs a full clean rebuild under `bomtrace3` on every invocation.
The ~20% runtime overhead of `bomtrace3` means the rebuild takes roughly
1.2× the normal build time.
The raw logfile (`bomsh_raw_logfile.sha1`) and conf file (`_bomsh.conf`)
are written to the build directory and removed by `make clean`. The
`omnibor/` tree is also removed by `make clean`.
#### Identity of the SHA-256 in the enriched SPDX
`make bomsh` discovers the bomsh-traced library under
`$(abs_builddir)/src/.libs/` and passes it to the nested `make sbom`
invocation as `SBOM_LIB_OVERRIDE`, so the SHA-256 in the SPDX
`checksums[]` is the SHA-256 of the **exact binary that `bomtrace3`
traced**. Without that override `make sbom` would re-link via `make
install DESTDIR=...` and hash a libtool-relinked artefact whose
SHA-256 differs from the traced library, leaving the SHA-256 in
`checksums[]` and the OmniBOR `externalRefs` describing two different
files in the same SPDX document.
#### CI verifiability gates
The bomsh CI job enforces two independent self-consistency properties
on every PR, in addition to schema validation of the enriched SPDX
through `pyspdxtools`:
1. **Resolvability** — every `gitoid` listed in the SPDX `externalRefs`
resolves to a blob present at `omnibor/objects/<aa>/<rest>`.
2. **Object-store integrity** — every blob in `omnibor/objects/`
round-trips through `sha1(b"blob <len>\0" + content)`, so a corrupt
or truncated object store is caught at PR time, not by a downstream
verifier weeks later.
If either fails, the PR fails — the bomsh provenance bundle that a
CRA reviewer would download is never published with a broken bridge.
A third gate is **not** implemented: the gitoid that `bomsh_sbom.py`
attaches to the SPDX is the bom_id of the OmniBOR Input Manifest for
the traced artefact, not the git-blob hash of the binary itself. The
two are different by design (the bom_id summarises the build inputs,
not the linked output bytes), so a "gitoid == sha1 of the binary"
check would always fail. What ties the SBOM to the binary today is
the SHA-256 in `checksums[]`, which the `SBOM_LIB_OVERRIDE` plumbing
described above guarantees is the SHA-256 of the bomsh-traced library.
The verifier itself lives at `scripts/bomsh_verify.py` (importable, with
synthetic-fixture unit tests in `scripts/test_gen_sbom.py`). Run it
against any local `make bomsh` output with:
```sh
python3 scripts/bomsh_verify.py
```
---
## 4. Combined workflow
Running both targets produces the complete set of supply chain transparency
artefacts. `make bomsh` automatically enriches the SPDX document from
`make sbom` if it is present; there is no need to pass any extra flags.
```sh
./configure
make
make sbom # component identity
make bomsh # build provenance + enriched SPDX
```
All output files:
| File | From | Description |
|---|---|---|
| `wolfssl-<ver>.cdx.json` | `make sbom` | CycloneDX 1.6 component SBOM |
| `wolfssl-<ver>.spdx.json` | `make sbom` | SPDX 2.3 JSON component SBOM |
| `wolfssl-<ver>.spdx` | `make sbom` | SPDX 2.3 tag-value, validated |
| `omnibor/` | `make bomsh` | OmniBOR artifact dependency graph |
| `omnibor.wolfssl-<ver>.spdx.json` | `make bomsh` | SPDX 2.3 JSON enriched with OmniBOR gitoid |
The enriched SPDX is the document to hand to a CRA auditor or downstream
consumer when you want both component identity and build traceability in one
file.
---
## 5. Using wolfSSL's artefacts in a product
If you are shipping a product that includes wolfSSL and need to satisfy CRA
obligations, see `doc/CRA.md` for guidance on integrating these artefacts
into your product SBOM and what to provide to a conformity assessor.
If a vulnerability is found in wolfSSL itself or in any dependency listed
in the SBOM, see [`SECURITY-POLICY.md`](../SECURITY-POLICY.md) at the
repository root for wolfSSL's disclosure process, severity rubric, and
coordinated-disclosure practice. Reports use
[`SECURITY-REPORT-TEMPLATE.md`](../SECURITY-REPORT-TEMPLATE.md) and go to
**support@wolfssl.com**; published advisories are at
<https://www.wolfssl.com/docs/security-vulnerabilities/>.
+4 -1
View File
@@ -4,7 +4,9 @@
dist_doc_DATA+= doc/README.txt \
doc/QUIC.md \
doc/dilithium-to-mldsa-migration.md
doc/dilithium-to-mldsa-migration.md \
doc/SBOM.md \
doc/CRA.md
dox-pdf:
@@ -22,3 +24,4 @@ clean-local:
-rm -rf doc/html/
-rm -f doc/refman.pdf
-rm -f doc/doxygen_warnings
-rm -rf $(BOMSH_OMNIBORDIR)
+45
View File
@@ -0,0 +1,45 @@
{
"_comment": "Human-authored VEX determinations keyed by CVE id. The CVE Program record carries the structural facts (CWE/CVSS/affected ranges); this overlay carries what it cannot express in machine-readable form: the analysis state, the not-affected justification, the response, free-text scope detail, the mainline fixed release version(s), an optional separately-modelled FIPS product entry, and an optional no-cost build-reachability hedge (requires_defines / default_status). The FIPS module_version and cmvp_cert values below are ILLUSTRATIVE placeholders; substitute the real validated module version and CMVP certificate number. gen-advisory folds these into both the CSAF and CycloneDX VEX outputs. requires_defines/default_status are recorded as informational notes only -- this tool does not compute per-build reachability.",
"CVE-2026-5501": {
"state": "exploitable",
"response": ["update"],
"detail": "Limited to applications using the OpenSSL compatibility API directly (wolfSSL_X509_verify_cert), such as nginx and haproxy integrations. The native wolfSSL TLS handshake path (ProcessPeerCerts) is not susceptible.",
"fixed_versions": ["5.9.1"],
"remediation": "Update to wolfSSL 5.9.1 or later, or avoid relying on wolfSSL_X509_verify_cert in the OpenSSL compatibility layer for chain validation.",
"fips": {
"name": "wolfCrypt FIPS 140-3 Module",
"module_version": "5.2.1",
"cmvp_cert": "4718",
"status": "not_affected",
"justification": "code_not_present",
"remediation": "No action required for the FIPS-validated module: the affected OpenSSL compatibility layer (wolfSSL_X509_verify_cert) is outside the wolfCrypt FIPS module boundary."
}
},
"CVE-2026-5778": {
"state": "exploitable",
"response": ["update"],
"detail": "Integer underflow in the ChaCha20-Poly1305 decryption path of the packet sniffer.",
"requires_defines": ["WOLFSSL_SNIFFER", "HAVE_CHACHA", "HAVE_POLY1305"],
"default_status": "off",
"fixed_versions": ["5.9.1"],
"remediation": "Update to wolfSSL 5.9.1 or later. Builds without --enable-sniffer are not affected.",
"fips": {
"name": "wolfCrypt FIPS 140-3 Module",
"module_version": "5.2.1",
"cmvp_cert": "4718",
"status": "not_affected",
"justification": "code_not_present",
"remediation": "No action required for the FIPS-validated module: the packet sniffer (src/sniffer.c) is outside the wolfCrypt FIPS module boundary."
}
},
"CVE-2026-5999": {
"state": "exploitable",
"response": ["update"],
"detail": "Synthetic fixture overlay: a simple mainline-only finding (no separate FIPS product) used to exercise the CVSS v3.1 scores[] path.",
"fixed_versions": ["5.9.1"],
"remediation": "Update to wolfSSL 5.9.1 or later."
}
}
+118
View File
@@ -0,0 +1,118 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://www.wolfssl.com/schema/advisory-vex-overlay-1.json",
"title": "wolfSSL gen-advisory VEX overlay",
"description": "Human-authored VEX determinations keyed by CVE id, consumed by scripts/gen-advisory. The CVE Program record supplies the structural facts (CWE/CVSS/affected ranges); this overlay supplies what the record cannot express in machine-readable form. Enum values mirror the CycloneDX 1.6 vulnerability analysis vocabulary so the same terms map cleanly into both the CSAF and CycloneDX VEX outputs.",
"type": "object",
"properties": {
"_comment": {
"type": "string",
"description": "Free-text note ignored by gen-advisory."
}
},
"patternProperties": {
"^CVE-[0-9]{4}-[0-9]{4,}$": { "$ref": "#/$defs/overlayEntry" }
},
"additionalProperties": false,
"$defs": {
"analysisState": {
"type": "string",
"description": "CycloneDX 1.6 vulnerability analysis state.",
"enum": [
"resolved",
"resolved_with_pedigree",
"exploitable",
"in_triage",
"false_positive",
"not_affected"
]
},
"justification": {
"type": "string",
"description": "CycloneDX 1.6 impact analysis justification (required by gen-advisory when state is not_affected so a CSAF flag can be emitted).",
"enum": [
"code_not_present",
"code_not_reachable",
"requires_configuration",
"requires_dependency",
"requires_environment",
"protected_by_compiler",
"protected_at_perimeter",
"protected_at_runtime",
"protected_by_mitigating_control"
]
},
"response": {
"type": "array",
"description": "CycloneDX 1.6 vulnerability analysis response.",
"items": {
"type": "string",
"enum": [
"can_not_fix",
"will_not_fix",
"update",
"rollback",
"workaround_available"
]
}
},
"versionList": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"minItems": 1
},
"fips": {
"type": "object",
"description": "Optional separately-modelled FIPS product entry. FIPS customers cannot freely upgrade and many CVEs fall outside the validated module boundary, so FIPS is modelled as its own product with its own status and remediation.",
"properties": {
"name": { "type": "string", "minLength": 1 },
"module_version": { "type": "string", "minLength": 1 },
"cmvp_cert": {
"type": "string",
"minLength": 1,
"description": "CMVP certificate number, recorded as a CSAF model_number / CycloneDX property."
},
"status": { "$ref": "#/$defs/analysisState" },
"justification": { "$ref": "#/$defs/justification" },
"fixed_versions": { "$ref": "#/$defs/versionList" },
"remediation": { "type": "string", "minLength": 1 }
},
"additionalProperties": false,
"allOf": [
{
"if": { "properties": { "status": { "const": "not_affected" } }, "required": ["status"] },
"then": { "required": ["justification"] }
}
]
},
"overlayEntry": {
"type": "object",
"properties": {
"state": { "$ref": "#/$defs/analysisState" },
"justification": { "$ref": "#/$defs/justification" },
"response": { "$ref": "#/$defs/response" },
"detail": { "type": "string" },
"fixed_versions": { "$ref": "#/$defs/versionList" },
"remediation": { "type": "string", "minLength": 1 },
"requires_defines": {
"type": "array",
"items": { "type": "string", "minLength": 1 },
"description": "Build flags that gate the vulnerable code. Recorded as an informational note only; gen-advisory does NOT compute per-build reachability."
},
"default_status": {
"type": "string",
"enum": ["on", "off", "enabled", "disabled"]
},
"fips": { "$ref": "#/$defs/fips" }
},
"required": ["state"],
"additionalProperties": false,
"allOf": [
{
"if": { "properties": { "state": { "const": "not_affected" } }, "required": ["state"] },
"then": { "required": ["justification"] }
}
]
}
}
}
+217
View File
@@ -0,0 +1,217 @@
#!/usr/bin/env python3
"""End-to-end verifier for the bomsh provenance bundle.
Two independent self-consistency checks on the artefacts that
`make bomsh` produces. The PERSISTENT-ID assertion in the bomsh CI
job only proves the gitoid externalRef *exists* in the enriched SPDX;
neither of these follow-up properties is guaranteed by it:
(A) Resolvability -- every gitoid in the SPDX externalRefs resolves
to a blob present at omnibor/objects/<aa>/<rest>. Catches the
`bomsh_sbom.py` regression class that emits a syntactically
well-formed gitoid which does not actually point at anything in
the shipped ADG.
(B) Object-store integrity -- every blob in omnibor/objects/
round-trips through sha1(b"blob <len>\\0" + content), so a
corrupt or truncated object store is caught at PR time, not by
a downstream verifier weeks later.
CLI form (used by `.github/workflows/sbom.yml`):
python3 scripts/bomsh_verify.py \\
--spdx-glob 'omnibor.wolfssl-*.spdx.json' \\
--omnibor-dir omnibor
Library form (used by scripts/test_gen_sbom.py):
from scripts import bomsh_verify
ok, messages = bomsh_verify.verify(...)
"""
import argparse
import glob as _glob
import hashlib
import json
import os
import re
import sys
from typing import List
GITOID_LOCATOR_PREFIX = 'gitoid:blob:sha1:'
# An OmniBOR sha1 gitoid is exactly 40 lowercase-hex chars. Validate before
# the value is used to build an object path: a crafted SPDX with a locator like
# 'gitoid:blob:sha1:../../../etc/shadow' otherwise passes the prefix check and
# turns the os.path.join() below into a path-traversal existence oracle.
_SHA1_HEX_RE = re.compile(r'[0-9a-f]{40}\Z')
def gitoid_sha1(path):
"""OmniBOR `gitoid:blob:sha1:<hex>` is the canonical Git blob hash:
sha1(b"blob <len>\\0" + content). Symlinks are followed transparently
by `open()`, which matches what bomsh records (the trace sees the
target, not the symlink)."""
with open(path, 'rb') as f:
data = f.read()
h = hashlib.sha1()
h.update(f'blob {len(data)}\0'.encode())
h.update(data)
return h.hexdigest()
def load_spdx_gitoids(spdx_path):
"""Return [(package_name, gitoid_hex), ...] for every externalRef
of referenceType 'gitoid' in the SPDX document at spdx_path.
Raises ValueError on a malformed locator (anything that isn't
`gitoid:blob:sha1:<hex>`). An sha256 locator would land here too
if bomsh ever switches; the failure is the right behaviour, since
a maintainer must update the verifier in lockstep."""
with open(spdx_path) as f:
spdx = json.load(f)
gitoids = []
for pkg in spdx.get('packages', []):
for ref in pkg.get('externalRefs', []):
if ref.get('referenceType') != 'gitoid':
continue
loc = ref.get('referenceLocator', '')
if not loc.startswith(GITOID_LOCATOR_PREFIX):
raise ValueError(
f'unexpected gitoid locator format: {loc!r} '
f'(expected {GITOID_LOCATOR_PREFIX}<hex>; if bomsh '
f'has switched to sha256 the verifier needs updating)')
gid = loc[len(GITOID_LOCATOR_PREFIX):]
if not _SHA1_HEX_RE.match(gid):
raise ValueError(
f'malformed gitoid {gid!r} in locator {loc!r}: expected '
f'40 lowercase-hex sha1 characters (refusing to use it in '
f'an object path)')
gitoids.append((pkg.get('name', '<no-name>'), gid))
return gitoids
def check_resolvability(spdx_gitoids, omnibor_objects_dir):
"""(A) Every SPDX gitoid resolves to a file at
`<omnibor_objects_dir>/<aa>/<rest>`. Returns a list of
(pkg_name, gitoid, expected_path) for the unresolved ones; empty
list means every gitoid resolved."""
missing = []
for pkg_name, gid in spdx_gitoids:
obj = os.path.join(omnibor_objects_dir, gid[:2], gid[2:])
if not os.path.isfile(obj):
missing.append((pkg_name, gid, obj))
return missing
_HEX_CHARS = frozenset('0123456789abcdef')
def _looks_like_blob_path(parts):
"""True iff `parts` is the canonical `<aa>/<rest>` shape Git uses
for content-addressed blob fanout: exactly two components, the
first of which is a 2-char lowercase-hex prefix and the second of
which is the remaining lowercase-hex of a sha1 digest (38 chars)
or sha256 digest (62 chars). Anything else (`info/`, `pack/...`,
deeper nesting) is housekeeping and must NOT be gitoid-checked."""
if len(parts) != 2:
return False
aa, rest = parts
if len(aa) != 2 or not all(c in _HEX_CHARS for c in aa):
return False
if len(rest) not in (38, 62):
return False
return all(c in _HEX_CHARS for c in rest)
def check_object_store_integrity(omnibor_objects_dir):
"""(B) Every blob in <omnibor_objects_dir> round-trips through
`gitoid_sha1`. Returns (count_total, [(path, expected, actual), ...]
for blobs whose content does not match their expected gitoid).
The directory layout is `<omnibor_objects_dir>/<aa>/<rest>` (Git's
standard fanout, where <aa> is the first two hex chars of the
digest); files outside that shape are skipped silently (e.g.
`info/` or `pack/` siblings, README files, etc.)."""
bad = []
obj_count = 0
for root, _, files in os.walk(omnibor_objects_dir):
for fname in files:
obj = os.path.join(root, fname)
rel = os.path.relpath(obj, omnibor_objects_dir)
parts = rel.split(os.sep)
if not _looks_like_blob_path(parts):
continue
expected = parts[0] + parts[1]
obj_count += 1
actual = gitoid_sha1(obj)
if actual != expected:
bad.append((obj, expected, actual))
return obj_count, bad
def verify(spdx_glob, omnibor_dir):
"""Orchestrate the two checks. Returns (ok: bool, messages:
List[str]). `messages` is appended to in success and failure both,
so callers can log the success lines ('OK: N gitoid(s) verified' +
' objects round-trip: M blobs') even when ok is True."""
messages: List[str] = []
spdx_paths = sorted(_glob.glob(spdx_glob))
if not spdx_paths:
return False, [f'no SPDX matched {spdx_glob!r}']
spdx_path = spdx_paths[0]
try:
spdx_gitoids = load_spdx_gitoids(spdx_path)
except (json.JSONDecodeError, ValueError) as e:
return False, [f'could not load SPDX gitoids: {e}']
if not spdx_gitoids:
return False, [f'no gitoid externalRefs in {spdx_path}']
objects_dir = os.path.join(omnibor_dir, 'objects')
missing = check_resolvability(spdx_gitoids, objects_dir)
if missing:
for pkg_name, gid, obj in missing:
messages.append(
f'DANGLING: {pkg_name} gitoid {gid} -> {obj}')
messages.append(
f'{len(missing)} SPDX gitoid(s) not present in '
f'{objects_dir}/ (provenance bundle is broken)')
return False, messages
obj_count, bad = check_object_store_integrity(objects_dir)
if bad:
for obj, expected, actual in bad[:5]:
messages.append(
f'CORRUPT: {obj} expected {expected} got {actual}')
messages.append(
f'{len(bad)} object(s) in {objects_dir}/ failed gitoid '
f'round-trip (object store is corrupt)')
return False, messages
messages.append(f'OK: {len(spdx_gitoids)} gitoid(s) verified')
messages.append(f' objects round-trip: {obj_count} blobs')
return True, messages
def main():
parser = argparse.ArgumentParser(
description='End-to-end verifier for the bomsh provenance bundle.')
parser.add_argument('--spdx-glob',
default='omnibor.wolfssl-*.spdx.json',
help='Glob matching the bomsh-enriched SPDX file '
'(default: %(default)s)')
parser.add_argument('--omnibor-dir', default='omnibor',
help='Path to the OmniBOR directory containing '
'objects/ (default: %(default)s)')
args = parser.parse_args()
ok, messages = verify(args.spdx_glob, args.omnibor_dir)
for line in messages:
print(line, file=sys.stderr if not ok else sys.stdout)
sys.exit(0 if ok else 1)
if __name__ == '__main__':
main()
+83
View File
@@ -0,0 +1,83 @@
// CSAF 2.0 conformance gate for documents emitted by scripts/gen-advisory.
//
// JSON-schema validity is necessary but NOT sufficient for CSAF: the standard
// defines a battery of *mandatory tests* (section 6.1.*) -- CVSS/vector
// consistency, contradicting product status, product_id defined/used,
// tracking.version vs revision_history, and so on -- that a bare schema pass
// happily accepts. This runner uses the Secvisogram reference implementation
// (@secvisogram/csaf-validator-lib) which bundles every schema (incl. the
// first.org CVSS schemas) and implements those mandatory tests, so the check
// is fully offline and reproducible once the pinned dependency is installed.
//
// Gate = the strict CSAF 2.0 schema test + all mandatory tests. Optional and
// informative tests are reported as warnings only (they encode house-style
// preferences, not conformance).
//
// Usage: node scripts/csaf_validate.mjs <doc.csaf.json> [<doc2.csaf.json> ...]
// Exit 0 if every document passes the gate, 1 otherwise.
import { readFileSync } from 'node:fs'
import validate from '@secvisogram/csaf-validator-lib/validate.js'
import * as schemaTests from '@secvisogram/csaf-validator-lib/schemaTests.js'
import * as mandatoryTests from '@secvisogram/csaf-validator-lib/mandatoryTests.js'
import * as optionalTests from '@secvisogram/csaf-validator-lib/optionalTests.js'
const files = process.argv.slice(2)
if (files.length === 0) {
console.error('usage: node scripts/csaf_validate.mjs <doc.csaf.json> ...')
process.exit(2)
}
// The gate: strict 2.0 schema + every mandatory test.
const gateTests = [schemaTests.csaf_2_0_strict, ...Object.values(mandatoryTests)]
// Reported for visibility but non-fatal.
const advisoryTests = [...Object.values(optionalTests)]
function summarize(testResults) {
// testResults: [{ name, isValid, errors, warnings, infos }]
const failed = []
for (const t of testResults) {
if (t.isValid === false || (t.errors && t.errors.length > 0)) {
failed.push(t)
}
}
return failed
}
let anyInvalid = false
for (const file of files) {
let doc
try {
doc = JSON.parse(readFileSync(file, 'utf8'))
} catch (e) {
console.error(`ERROR: cannot read/parse ${file}: ${e.message}`)
anyInvalid = true
continue
}
const gate = await validate(gateTests, doc)
const advisory = await validate(advisoryTests, doc)
if (gate.isValid) {
console.log(`OK ${file} (strict schema + ${Object.keys(mandatoryTests).length} mandatory tests)`)
} else {
anyInvalid = true
console.error(`FAIL ${file}`)
for (const t of summarize(gate.tests)) {
for (const err of t.errors || []) {
console.error(` [${t.name}] ${err.instancePath || '/'}: ${err.message}`)
}
}
}
// Surface optional-test warnings without failing the build.
const optWarn = summarize(advisory.tests)
for (const t of optWarn) {
for (const err of t.errors || []) {
console.warn(` warn ${file} [${t.name}] ${err.instancePath || '/'}: ${err.message}`)
}
}
}
process.exit(anyInvalid ? 1 : 0)
+971
View File
@@ -0,0 +1,971 @@
{
"CWE-1004": "Sensitive Cookie Without 'HttpOnly' Flag",
"CWE-1007": "Insufficient Visual Distinction of Homoglyphs Presented to User",
"CWE-102": "Struts: Duplicate Validation Forms",
"CWE-1021": "Improper Restriction of Rendered UI Layers or Frames",
"CWE-1022": "Use of Web Link to Untrusted Target with window.opener Access",
"CWE-1023": "Incomplete Comparison with Missing Factors",
"CWE-1024": "Comparison of Incompatible Types",
"CWE-1025": "Comparison Using Wrong Factors",
"CWE-103": "Struts: Incomplete validate() Method Definition",
"CWE-1037": "Processor Optimization Removal or Modification of Security-critical Code",
"CWE-1038": "Insecure Automated Optimizations",
"CWE-1039": "Inadequate Detection or Handling of Adversarial Input Perturbations in Automated Recognition Mechanism",
"CWE-104": "Struts: Form Bean Does Not Extend Validation Class",
"CWE-1041": "Use of Redundant Code",
"CWE-1042": "Static Member Data Element outside of a Singleton Class Element",
"CWE-1043": "Data Element Aggregating an Excessively Large Number of Non-Primitive Elements",
"CWE-1044": "Architecture with Number of Horizontal Layers Outside of Expected Range",
"CWE-1045": "Parent Class with a Virtual Destructor and a Child Class without a Virtual Destructor",
"CWE-1046": "Creation of Immutable Text Using String Concatenation",
"CWE-1047": "Modules with Circular Dependencies",
"CWE-1048": "Invokable Control Element with Large Number of Outward Calls",
"CWE-1049": "Excessive Data Query Operations in a Large Data Table",
"CWE-105": "Struts: Form Field Without Validator",
"CWE-1050": "Excessive Platform Resource Consumption within a Loop",
"CWE-1051": "Initialization with Hard-Coded Network Resource Configuration Data",
"CWE-1052": "Excessive Use of Hard-Coded Literals in Initialization",
"CWE-1053": "Missing Documentation for Design",
"CWE-1054": "Invocation of a Control Element at an Unnecessarily Deep Horizontal Layer",
"CWE-1055": "Multiple Inheritance from Concrete Classes",
"CWE-1056": "Invokable Control Element with Variadic Parameters",
"CWE-1057": "Data Access Operations Outside of Expected Data Manager Component",
"CWE-1058": "Invokable Control Element in Multi-Thread Context with non-Final Static Storable or Member Element",
"CWE-1059": "Insufficient Technical Documentation",
"CWE-106": "Struts: Plug-in Framework not in Use",
"CWE-1060": "Excessive Number of Inefficient Server-Side Data Accesses",
"CWE-1061": "Insufficient Encapsulation",
"CWE-1062": "Parent Class with References to Child Class",
"CWE-1063": "Creation of Class Instance within a Static Code Block",
"CWE-1064": "Invokable Control Element with Signature Containing an Excessive Number of Parameters",
"CWE-1065": "Runtime Resource Management Control Element in a Component Built to Run on Application Servers",
"CWE-1066": "Missing Serialization Control Element",
"CWE-1067": "Excessive Execution of Sequential Searches of Data Resource",
"CWE-1068": "Inconsistency Between Implementation and Documented Design",
"CWE-1069": "Empty Exception Block",
"CWE-107": "Struts: Unused Validation Form",
"CWE-1070": "Serializable Data Element Containing non-Serializable Item Elements",
"CWE-1071": "Empty Code Block",
"CWE-1072": "Data Resource Access without Use of Connection Pooling",
"CWE-1073": "Non-SQL Invokable Control Element with Excessive Number of Data Resource Accesses",
"CWE-1074": "Class with Excessively Deep Inheritance",
"CWE-1075": "Unconditional Control Flow Transfer outside of Switch Block",
"CWE-1076": "Insufficient Adherence to Expected Conventions",
"CWE-1077": "Floating Point Comparison with Incorrect Operator",
"CWE-1078": "Inappropriate Source Code Style or Formatting",
"CWE-1079": "Parent Class without Virtual Destructor Method",
"CWE-108": "Struts: Unvalidated Action Form",
"CWE-1080": "Source Code File with Excessive Number of Lines of Code",
"CWE-1082": "Class Instance Self Destruction Control Element",
"CWE-1083": "Data Access from Outside Expected Data Manager Component",
"CWE-1084": "Invokable Control Element with Excessive File or Data Access Operations",
"CWE-1085": "Invokable Control Element with Excessive Volume of Commented-out Code",
"CWE-1086": "Class with Excessive Number of Child Classes",
"CWE-1087": "Class with Virtual Method without a Virtual Destructor",
"CWE-1088": "Synchronous Access of Remote Resource without Timeout",
"CWE-1089": "Large Data Table with Excessive Number of Indices",
"CWE-109": "Struts: Validator Turned Off",
"CWE-1090": "Method Containing Access of a Member Element from Another Class",
"CWE-1091": "Use of Object without Invoking Destructor Method",
"CWE-1092": "Use of Same Invokable Control Element in Multiple Architectural Layers",
"CWE-1093": "Excessively Complex Data Representation",
"CWE-1094": "Excessive Index Range Scan for a Data Resource",
"CWE-1095": "Loop Condition Value Update within the Loop",
"CWE-1096": "Singleton Class Instance Creation without Proper Locking or Synchronization",
"CWE-1097": "Persistent Storable Data Element without Associated Comparison Control Element",
"CWE-1098": "Data Element containing Pointer Item without Proper Copy Control Element",
"CWE-1099": "Inconsistent Naming Conventions for Identifiers",
"CWE-11": "ASP.NET Misconfiguration: Creating Debug Binary",
"CWE-110": "Struts: Validator Without Form Field",
"CWE-1100": "Insufficient Isolation of System-Dependent Functions",
"CWE-1101": "Reliance on Runtime Component in Generated Code",
"CWE-1102": "Reliance on Machine-Dependent Data Representation",
"CWE-1103": "Use of Platform-Dependent Third Party Components",
"CWE-1104": "Use of Unmaintained Third Party Components",
"CWE-1105": "Insufficient Encapsulation of Machine-Dependent Functionality",
"CWE-1106": "Insufficient Use of Symbolic Constants",
"CWE-1107": "Insufficient Isolation of Symbolic Constant Definitions",
"CWE-1108": "Excessive Reliance on Global Variables",
"CWE-1109": "Use of Same Variable for Multiple Purposes",
"CWE-111": "Direct Use of Unsafe JNI",
"CWE-1110": "Incomplete Design Documentation",
"CWE-1111": "Incomplete I/O Documentation",
"CWE-1112": "Incomplete Documentation of Program Execution",
"CWE-1113": "Inappropriate Comment Style",
"CWE-1114": "Inappropriate Whitespace Style",
"CWE-1115": "Source Code Element without Standard Prologue",
"CWE-1116": "Inaccurate Comments",
"CWE-1117": "Callable with Insufficient Behavioral Summary",
"CWE-1118": "Insufficient Documentation of Error Handling Techniques",
"CWE-1119": "Excessive Use of Unconditional Branching",
"CWE-112": "Missing XML Validation",
"CWE-1120": "Excessive Code Complexity",
"CWE-1121": "Excessive McCabe Cyclomatic Complexity",
"CWE-1122": "Excessive Halstead Complexity",
"CWE-1123": "Excessive Use of Self-Modifying Code",
"CWE-1124": "Excessively Deep Nesting",
"CWE-1125": "Excessive Attack Surface",
"CWE-1126": "Declaration of Variable with Unnecessarily Wide Scope",
"CWE-1127": "Compilation with Insufficient Warnings or Errors",
"CWE-113": "Improper Neutralization of CRLF Sequences in HTTP Headers ('HTTP Request/Response Splitting')",
"CWE-114": "Process Control",
"CWE-115": "Misinterpretation of Input",
"CWE-116": "Improper Encoding or Escaping of Output",
"CWE-1164": "Irrelevant Code",
"CWE-117": "Improper Output Neutralization for Logs",
"CWE-1173": "Improper Use of Validation Framework",
"CWE-1174": "ASP.NET Misconfiguration: Improper Model Validation",
"CWE-1176": "Inefficient CPU Computation",
"CWE-1177": "Use of Prohibited Code",
"CWE-118": "Incorrect Access of Indexable Resource ('Range Error')",
"CWE-1187": "DEPRECATED: Use of Uninitialized Resource",
"CWE-1188": "Initialization of a Resource with an Insecure Default",
"CWE-1189": "Improper Isolation of Shared Resources on System-on-a-Chip (SoC)",
"CWE-119": "Improper Restriction of Operations within the Bounds of a Memory Buffer",
"CWE-1190": "DMA Device Enabled Too Early in Boot Phase",
"CWE-1191": "On-Chip Debug and Test Interface With Improper Access Control",
"CWE-1192": "Improper Identifier for IP Block used in System-On-Chip (SOC)",
"CWE-1193": "Power-On of Untrusted Execution Core Before Enabling Fabric Access Control",
"CWE-12": "ASP.NET Misconfiguration: Missing Custom Error Page",
"CWE-120": "Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')",
"CWE-1204": "Generation of Weak Initialization Vector (IV)",
"CWE-1209": "Failure to Disable Reserved Bits",
"CWE-121": "Stack-based Buffer Overflow",
"CWE-122": "Heap-based Buffer Overflow",
"CWE-1220": "Insufficient Granularity of Access Control",
"CWE-1221": "Incorrect Register Defaults or Module Parameters",
"CWE-1222": "Insufficient Granularity of Address Regions Protected by Register Locks",
"CWE-1223": "Race Condition for Write-Once Attributes",
"CWE-1224": "Improper Restriction of Write-Once Bit Fields",
"CWE-1229": "Creation of Emergent Resource",
"CWE-123": "Write-what-where Condition",
"CWE-1230": "Exposure of Sensitive Information Through Metadata",
"CWE-1231": "Improper Prevention of Lock Bit Modification",
"CWE-1232": "Improper Lock Behavior After Power State Transition",
"CWE-1233": "Security-Sensitive Hardware Controls with Missing Lock Bit Protection",
"CWE-1234": "Hardware Internal or Debug Modes Allow Override of Locks",
"CWE-1235": "Incorrect Use of Autoboxing and Unboxing for Performance Critical Operations",
"CWE-1236": "Improper Neutralization of Formula Elements in a CSV File",
"CWE-1239": "Improper Zeroization of Hardware Register",
"CWE-124": "Buffer Underwrite ('Buffer Underflow')",
"CWE-1240": "Use of a Cryptographic Primitive with a Risky Implementation",
"CWE-1241": "Use of Predictable Algorithm in Random Number Generator",
"CWE-1242": "Inclusion of Undocumented Features or Chicken Bits",
"CWE-1243": "Sensitive Non-Volatile Information Not Protected During Debug",
"CWE-1244": "Internal Asset Exposed to Unsafe Debug Access Level or State",
"CWE-1245": "Improper Finite State Machines (FSMs) in Hardware Logic",
"CWE-1246": "Improper Write Handling in Limited-write Non-Volatile Memories",
"CWE-1247": "Improper Protection Against Voltage and Clock Glitches",
"CWE-1248": "Semiconductor Defects in Hardware Logic with Security-Sensitive Implications",
"CWE-1249": "Application-Level Admin Tool with Inconsistent View of Underlying Operating System",
"CWE-125": "Out-of-bounds Read",
"CWE-1250": "Improper Preservation of Consistency Between Independent Representations of Shared State",
"CWE-1251": "Mirrored Regions with Different Values",
"CWE-1252": "CPU Hardware Not Configured to Support Exclusivity of Write and Execute Operations",
"CWE-1253": "Incorrect Selection of Fuse Values",
"CWE-1254": "Incorrect Comparison Logic Granularity",
"CWE-1255": "Comparison Logic is Vulnerable to Power Side-Channel Attacks",
"CWE-1256": "Improper Restriction of Software Interfaces to Hardware Features",
"CWE-1257": "Improper Access Control Applied to Mirrored or Aliased Memory Regions",
"CWE-1258": "Exposure of Sensitive System Information Due to Uncleared Debug Information",
"CWE-1259": "Improper Restriction of Security Token Assignment",
"CWE-126": "Buffer Over-read",
"CWE-1260": "Improper Handling of Overlap Between Protected Memory Ranges",
"CWE-1261": "Improper Handling of Single Event Upsets",
"CWE-1262": "Improper Access Control for Register Interface",
"CWE-1263": "Improper Physical Access Control",
"CWE-1264": "Hardware Logic with Insecure De-Synchronization between Control and Data Channels",
"CWE-1265": "Unintended Reentrant Invocation of Non-reentrant Code Via Nested Calls",
"CWE-1266": "Improper Scrubbing of Sensitive Data from Decommissioned Device",
"CWE-1267": "Policy Uses Obsolete Encoding",
"CWE-1268": "Policy Privileges are not Assigned Consistently Between Control and Data Agents",
"CWE-1269": "Product Released in Non-Release Configuration",
"CWE-127": "Buffer Under-read",
"CWE-1270": "Generation of Incorrect Security Tokens",
"CWE-1271": "Uninitialized Value on Reset for Registers Holding Security Settings",
"CWE-1272": "Sensitive Information Uncleared Before Debug/Power State Transition",
"CWE-1273": "Device Unlock Credential Sharing",
"CWE-1274": "Improper Access Control for Volatile Memory Containing Boot Code",
"CWE-1275": "Sensitive Cookie with Improper SameSite Attribute",
"CWE-1276": "Hardware Child Block Incorrectly Connected to Parent System",
"CWE-1277": "Firmware Not Updateable",
"CWE-1278": "Missing Protection Against Hardware Reverse Engineering Using Integrated Circuit (IC) Imaging Techniques",
"CWE-1279": "Cryptographic Operations are run Before Supporting Units are Ready",
"CWE-128": "Wrap-around Error",
"CWE-1280": "Access Control Check Implemented After Asset is Accessed",
"CWE-1281": "Sequence of Processor Instructions Leads to Unexpected Behavior",
"CWE-1282": "Assumed-Immutable Data is Stored in Writable Memory",
"CWE-1283": "Mutable Attestation or Measurement Reporting Data",
"CWE-1284": "Improper Validation of Specified Quantity in Input",
"CWE-1285": "Improper Validation of Specified Index, Position, or Offset in Input",
"CWE-1286": "Improper Validation of Syntactic Correctness of Input",
"CWE-1287": "Improper Validation of Specified Type of Input",
"CWE-1288": "Improper Validation of Consistency within Input",
"CWE-1289": "Improper Validation of Unsafe Equivalence in Input",
"CWE-129": "Improper Validation of Array Index",
"CWE-1290": "Incorrect Decoding of Security Identifiers",
"CWE-1291": "Public Key Re-Use for Signing both Debug and Production Code",
"CWE-1292": "Incorrect Conversion of Security Identifiers",
"CWE-1293": "Missing Source Correlation of Multiple Independent Data",
"CWE-1294": "Insecure Security Identifier Mechanism",
"CWE-1295": "Debug Messages Revealing Unnecessary Information",
"CWE-1296": "Incorrect Chaining or Granularity of Debug Components",
"CWE-1297": "Unprotected Confidential Information on Device is Accessible by OSAT Vendors",
"CWE-1298": "Hardware Logic Contains Race Conditions",
"CWE-1299": "Missing Protection Mechanism for Alternate Hardware Interface",
"CWE-13": "ASP.NET Misconfiguration: Password in Configuration File",
"CWE-130": "Improper Handling of Length Parameter Inconsistency",
"CWE-1300": "Improper Protection of Physical Side Channels",
"CWE-1301": "Insufficient or Incomplete Data Removal within Hardware Component",
"CWE-1302": "Missing Source Identifier in Entity Transactions on a System-On-Chip (SOC)",
"CWE-1303": "Non-Transparent Sharing of Microarchitectural Resources",
"CWE-1304": "Improperly Preserved Integrity of Hardware Configuration State During a Power Save/Restore Operation",
"CWE-131": "Incorrect Calculation of Buffer Size",
"CWE-1310": "Missing Ability to Patch ROM Code",
"CWE-1311": "Improper Translation of Security Attributes by Fabric Bridge",
"CWE-1312": "Missing Protection for Mirrored Regions in On-Chip Fabric Firewall",
"CWE-1313": "Hardware Allows Activation of Test or Debug Logic at Runtime",
"CWE-1314": "Missing Write Protection for Parametric Data Values",
"CWE-1315": "Improper Setting of Bus Controlling Capability in Fabric End-point",
"CWE-1316": "Fabric-Address Map Allows Programming of Unwarranted Overlaps of Protected and Unprotected Ranges",
"CWE-1317": "Improper Access Control in Fabric Bridge",
"CWE-1318": "Missing Support for Security Features in On-chip Fabrics or Buses",
"CWE-1319": "Improper Protection against Electromagnetic Fault Injection (EM-FI)",
"CWE-132": "DEPRECATED: Miscalculated Null Termination",
"CWE-1320": "Improper Protection for Outbound Error Messages and Alert Signals",
"CWE-1321": "Improperly Controlled Modification of Object Prototype Attributes ('Prototype Pollution')",
"CWE-1322": "Use of Blocking Code in Single-threaded, Non-blocking Context",
"CWE-1323": "Improper Management of Sensitive Trace Data",
"CWE-1324": "DEPRECATED: Sensitive Information Accessible by Physical Probing of JTAG Interface",
"CWE-1325": "Improperly Controlled Sequential Memory Allocation",
"CWE-1326": "Missing Immutable Root of Trust in Hardware",
"CWE-1327": "Binding to an Unrestricted IP Address",
"CWE-1328": "Security Version Number Mutable to Older Versions",
"CWE-1329": "Reliance on Component That is Not Updateable",
"CWE-1330": "Remanent Data Readable after Memory Erase",
"CWE-1331": "Improper Isolation of Shared Resources in Network On Chip (NoC)",
"CWE-1332": "Improper Handling of Faults that Lead to Instruction Skips",
"CWE-1333": "Inefficient Regular Expression Complexity",
"CWE-1334": "Unauthorized Error Injection Can Degrade Hardware Redundancy",
"CWE-1335": "Incorrect Bitwise Shift of Integer",
"CWE-1336": "Improper Neutralization of Special Elements Used in a Template Engine",
"CWE-1338": "Improper Protections Against Hardware Overheating",
"CWE-1339": "Insufficient Precision or Accuracy of a Real Number",
"CWE-134": "Use of Externally-Controlled Format String",
"CWE-1341": "Multiple Releases of Same Resource or Handle",
"CWE-1342": "Information Exposure through Microarchitectural State after Transient Execution",
"CWE-135": "Incorrect Calculation of Multi-Byte String Length",
"CWE-1351": "Improper Handling of Hardware Behavior in Exceptionally Cold Environments",
"CWE-1357": "Reliance on Insufficiently Trustworthy Component",
"CWE-138": "Improper Neutralization of Special Elements",
"CWE-1384": "Improper Handling of Physical or Environmental Conditions",
"CWE-1385": "Missing Origin Validation in WebSockets",
"CWE-1386": "Insecure Operation on Windows Junction / Mount Point",
"CWE-1389": "Incorrect Parsing of Numbers with Different Radices",
"CWE-1390": "Weak Authentication",
"CWE-1391": "Use of Weak Credentials",
"CWE-1392": "Use of Default Credentials",
"CWE-1393": "Use of Default Password",
"CWE-1394": "Use of Default Cryptographic Key",
"CWE-1395": "Dependency on Vulnerable Third-Party Component",
"CWE-14": "Compiler Removal of Code to Clear Buffers",
"CWE-140": "Improper Neutralization of Delimiters",
"CWE-141": "Improper Neutralization of Parameter/Argument Delimiters",
"CWE-1419": "Incorrect Initialization of Resource",
"CWE-142": "Improper Neutralization of Value Delimiters",
"CWE-1420": "Exposure of Sensitive Information during Transient Execution",
"CWE-1421": "Exposure of Sensitive Information in Shared Microarchitectural Structures during Transient Execution",
"CWE-1422": "Exposure of Sensitive Information caused by Incorrect Data Forwarding during Transient Execution",
"CWE-1423": "Exposure of Sensitive Information caused by Shared Microarchitectural Predictor State that Influences Transient Execution",
"CWE-1426": "Improper Validation of Generative AI Output",
"CWE-1427": "Improper Neutralization of Input Used for LLM Prompting",
"CWE-1428": "Reliance on HTTP instead of HTTPS",
"CWE-1429": "Missing Security-Relevant Feedback for Unexecuted Operations in Hardware Interface",
"CWE-143": "Improper Neutralization of Record Delimiters",
"CWE-1431": "Driving Intermediate Cryptographic State/Results to Hardware Module Outputs",
"CWE-1434": "Insecure Setting of Generative AI/ML Model Inference Parameters",
"CWE-144": "Improper Neutralization of Line Delimiters",
"CWE-145": "Improper Neutralization of Section Delimiters",
"CWE-146": "Improper Neutralization of Expression/Command Delimiters",
"CWE-147": "Improper Neutralization of Input Terminators",
"CWE-148": "Improper Neutralization of Input Leaders",
"CWE-149": "Improper Neutralization of Quoting Syntax",
"CWE-15": "External Control of System or Configuration Setting",
"CWE-150": "Improper Neutralization of Escape, Meta, or Control Sequences",
"CWE-151": "Improper Neutralization of Comment Delimiters",
"CWE-152": "Improper Neutralization of Macro Symbols",
"CWE-153": "Improper Neutralization of Substitution Characters",
"CWE-154": "Improper Neutralization of Variable Name Delimiters",
"CWE-155": "Improper Neutralization of Wildcards or Matching Symbols",
"CWE-156": "Improper Neutralization of Whitespace",
"CWE-157": "Failure to Sanitize Paired Delimiters",
"CWE-158": "Improper Neutralization of Null Byte or NUL Character",
"CWE-159": "Improper Handling of Invalid Use of Special Elements",
"CWE-160": "Improper Neutralization of Leading Special Elements",
"CWE-161": "Improper Neutralization of Multiple Leading Special Elements",
"CWE-162": "Improper Neutralization of Trailing Special Elements",
"CWE-163": "Improper Neutralization of Multiple Trailing Special Elements",
"CWE-164": "Improper Neutralization of Internal Special Elements",
"CWE-165": "Improper Neutralization of Multiple Internal Special Elements",
"CWE-166": "Improper Handling of Missing Special Element",
"CWE-167": "Improper Handling of Additional Special Element",
"CWE-168": "Improper Handling of Inconsistent Special Elements",
"CWE-170": "Improper Null Termination",
"CWE-172": "Encoding Error",
"CWE-173": "Improper Handling of Alternate Encoding",
"CWE-174": "Double Decoding of the Same Data",
"CWE-175": "Improper Handling of Mixed Encoding",
"CWE-176": "Improper Handling of Unicode Encoding",
"CWE-177": "Improper Handling of URL Encoding (Hex Encoding)",
"CWE-178": "Improper Handling of Case Sensitivity",
"CWE-179": "Incorrect Behavior Order: Early Validation",
"CWE-180": "Incorrect Behavior Order: Validate Before Canonicalize",
"CWE-181": "Incorrect Behavior Order: Validate Before Filter",
"CWE-182": "Collapse of Data into Unsafe Value",
"CWE-183": "Permissive List of Allowed Inputs",
"CWE-184": "Incomplete List of Disallowed Inputs",
"CWE-185": "Incorrect Regular Expression",
"CWE-186": "Overly Restrictive Regular Expression",
"CWE-187": "Partial String Comparison",
"CWE-188": "Reliance on Data/Memory Layout",
"CWE-190": "Integer Overflow or Wraparound",
"CWE-191": "Integer Underflow (Wrap or Wraparound)",
"CWE-192": "Integer Coercion Error",
"CWE-193": "Off-by-one Error",
"CWE-194": "Unexpected Sign Extension",
"CWE-195": "Signed to Unsigned Conversion Error",
"CWE-196": "Unsigned to Signed Conversion Error",
"CWE-197": "Numeric Truncation Error",
"CWE-198": "Use of Incorrect Byte Ordering",
"CWE-20": "Improper Input Validation",
"CWE-200": "Exposure of Sensitive Information to an Unauthorized Actor",
"CWE-201": "Insertion of Sensitive Information Into Sent Data",
"CWE-202": "Exposure of Sensitive Information Through Data Queries",
"CWE-203": "Observable Discrepancy",
"CWE-204": "Observable Response Discrepancy",
"CWE-205": "Observable Behavioral Discrepancy",
"CWE-206": "Observable Internal Behavioral Discrepancy",
"CWE-207": "Observable Behavioral Discrepancy With Equivalent Products",
"CWE-208": "Observable Timing Discrepancy",
"CWE-209": "Generation of Error Message Containing Sensitive Information",
"CWE-210": "Self-generated Error Message Containing Sensitive Information",
"CWE-211": "Externally-Generated Error Message Containing Sensitive Information",
"CWE-212": "Improper Removal of Sensitive Information Before Storage or Transfer",
"CWE-213": "Exposure of Sensitive Information Due to Incompatible Policies",
"CWE-214": "Invocation of Process Using Visible Sensitive Information",
"CWE-215": "Insertion of Sensitive Information Into Debugging Code",
"CWE-216": "DEPRECATED: Containment Errors (Container Errors)",
"CWE-217": "DEPRECATED: Failure to Protect Stored Data from Modification",
"CWE-218": "DEPRECATED: Failure to provide confidentiality for stored data",
"CWE-219": "Storage of File with Sensitive Data Under Web Root",
"CWE-22": "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')",
"CWE-220": "Storage of File With Sensitive Data Under FTP Root",
"CWE-221": "Information Loss or Omission",
"CWE-222": "Truncation of Security-relevant Information",
"CWE-223": "Omission of Security-relevant Information",
"CWE-224": "Obscured Security-relevant Information by Alternate Name",
"CWE-225": "DEPRECATED: General Information Management Problems",
"CWE-226": "Sensitive Information in Resource Not Removed Before Reuse",
"CWE-228": "Improper Handling of Syntactically Invalid Structure",
"CWE-229": "Improper Handling of Values",
"CWE-23": "Relative Path Traversal",
"CWE-230": "Improper Handling of Missing Values",
"CWE-231": "Improper Handling of Extra Values",
"CWE-232": "Improper Handling of Undefined Values",
"CWE-233": "Improper Handling of Parameters",
"CWE-234": "Failure to Handle Missing Parameter",
"CWE-235": "Improper Handling of Extra Parameters",
"CWE-236": "Improper Handling of Undefined Parameters",
"CWE-237": "Improper Handling of Structural Elements",
"CWE-238": "Improper Handling of Incomplete Structural Elements",
"CWE-239": "Failure to Handle Incomplete Element",
"CWE-24": "Path Traversal: '../filedir'",
"CWE-240": "Improper Handling of Inconsistent Structural Elements",
"CWE-241": "Improper Handling of Unexpected Data Type",
"CWE-242": "Use of Inherently Dangerous Function",
"CWE-243": "Creation of chroot Jail Without Changing Working Directory",
"CWE-244": "Improper Clearing of Heap Memory Before Release ('Heap Inspection')",
"CWE-245": "J2EE Bad Practices: Direct Management of Connections",
"CWE-246": "J2EE Bad Practices: Direct Use of Sockets",
"CWE-247": "DEPRECATED: Reliance on DNS Lookups in a Security Decision",
"CWE-248": "Uncaught Exception",
"CWE-249": "DEPRECATED: Often Misused: Path Manipulation",
"CWE-25": "Path Traversal: '/../filedir'",
"CWE-250": "Execution with Unnecessary Privileges",
"CWE-252": "Unchecked Return Value",
"CWE-253": "Incorrect Check of Function Return Value",
"CWE-256": "Plaintext Storage of a Password",
"CWE-257": "Storing Passwords in a Recoverable Format",
"CWE-258": "Empty Password in Configuration File",
"CWE-259": "Use of Hard-coded Password",
"CWE-26": "Path Traversal: '/dir/../filename'",
"CWE-260": "Password in Configuration File",
"CWE-261": "Weak Encoding for Password",
"CWE-262": "Not Using Password Aging",
"CWE-263": "Password Aging with Long Expiration",
"CWE-266": "Incorrect Privilege Assignment",
"CWE-267": "Privilege Defined With Unsafe Actions",
"CWE-268": "Privilege Chaining",
"CWE-269": "Improper Privilege Management",
"CWE-27": "Path Traversal: 'dir/../../filename'",
"CWE-270": "Privilege Context Switching Error",
"CWE-271": "Privilege Dropping / Lowering Errors",
"CWE-272": "Least Privilege Violation",
"CWE-273": "Improper Check for Dropped Privileges",
"CWE-274": "Improper Handling of Insufficient Privileges",
"CWE-276": "Incorrect Default Permissions",
"CWE-277": "Insecure Inherited Permissions",
"CWE-278": "Insecure Preserved Inherited Permissions",
"CWE-279": "Incorrect Execution-Assigned Permissions",
"CWE-28": "Path Traversal: '..\\filedir'",
"CWE-280": "Improper Handling of Insufficient Permissions or Privileges",
"CWE-281": "Improper Preservation of Permissions",
"CWE-282": "Improper Ownership Management",
"CWE-283": "Unverified Ownership",
"CWE-284": "Improper Access Control",
"CWE-285": "Improper Authorization",
"CWE-286": "Incorrect User Management",
"CWE-287": "Improper Authentication",
"CWE-288": "Authentication Bypass Using an Alternate Path or Channel",
"CWE-289": "Authentication Bypass by Alternate Name",
"CWE-29": "Path Traversal: '\\..\\filename'",
"CWE-290": "Authentication Bypass by Spoofing",
"CWE-291": "Reliance on IP Address for Authentication",
"CWE-292": "DEPRECATED: Trusting Self-reported DNS Name",
"CWE-293": "Using Referer Field for Authentication",
"CWE-294": "Authentication Bypass by Capture-replay",
"CWE-295": "Improper Certificate Validation",
"CWE-296": "Improper Following of a Certificate's Chain of Trust",
"CWE-297": "Improper Validation of Certificate with Host Mismatch",
"CWE-298": "Improper Validation of Certificate Expiration",
"CWE-299": "Improper Check for Certificate Revocation",
"CWE-30": "Path Traversal: '\\dir\\..\\filename'",
"CWE-300": "Channel Accessible by Non-Endpoint",
"CWE-301": "Reflection Attack in an Authentication Protocol",
"CWE-302": "Authentication Bypass by Assumed-Immutable Data",
"CWE-303": "Incorrect Implementation of Authentication Algorithm",
"CWE-304": "Missing Critical Step in Authentication",
"CWE-305": "Authentication Bypass by Primary Weakness",
"CWE-306": "Missing Authentication for Critical Function",
"CWE-307": "Improper Restriction of Excessive Authentication Attempts",
"CWE-308": "Use of Single-factor Authentication",
"CWE-309": "Use of Password System for Primary Authentication",
"CWE-31": "Path Traversal: 'dir\\..\\..\\filename'",
"CWE-311": "Missing Encryption of Sensitive Data",
"CWE-312": "Cleartext Storage of Sensitive Information",
"CWE-313": "Cleartext Storage in a File or on Disk",
"CWE-314": "Cleartext Storage in the Registry",
"CWE-315": "Cleartext Storage of Sensitive Information in a Cookie",
"CWE-316": "Cleartext Storage of Sensitive Information in Memory",
"CWE-317": "Cleartext Storage of Sensitive Information in GUI",
"CWE-318": "Cleartext Storage of Sensitive Information in Executable",
"CWE-319": "Cleartext Transmission of Sensitive Information",
"CWE-32": "Path Traversal: '...' (Triple Dot)",
"CWE-321": "Use of Hard-coded Cryptographic Key",
"CWE-322": "Key Exchange without Entity Authentication",
"CWE-323": "Reusing a Nonce, Key Pair in Encryption",
"CWE-324": "Use of a Key Past its Expiration Date",
"CWE-325": "Missing Cryptographic Step",
"CWE-326": "Inadequate Encryption Strength",
"CWE-327": "Use of a Broken or Risky Cryptographic Algorithm",
"CWE-328": "Use of Weak Hash",
"CWE-329": "Generation of Predictable IV with CBC Mode",
"CWE-33": "Path Traversal: '....' (Multiple Dot)",
"CWE-330": "Use of Insufficiently Random Values",
"CWE-331": "Insufficient Entropy",
"CWE-332": "Insufficient Entropy in PRNG",
"CWE-333": "Improper Handling of Insufficient Entropy in TRNG",
"CWE-334": "Small Space of Random Values",
"CWE-335": "Incorrect Usage of Seeds in Pseudo-Random Number Generator (PRNG)",
"CWE-336": "Same Seed in Pseudo-Random Number Generator (PRNG)",
"CWE-337": "Predictable Seed in Pseudo-Random Number Generator (PRNG)",
"CWE-338": "Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)",
"CWE-339": "Small Seed Space in PRNG",
"CWE-34": "Path Traversal: '....//'",
"CWE-340": "Generation of Predictable Numbers or Identifiers",
"CWE-341": "Predictable from Observable State",
"CWE-342": "Predictable Exact Value from Previous Values",
"CWE-343": "Predictable Value Range from Previous Values",
"CWE-344": "Use of Invariant Value in Dynamically Changing Context",
"CWE-345": "Insufficient Verification of Data Authenticity",
"CWE-346": "Origin Validation Error",
"CWE-347": "Improper Verification of Cryptographic Signature",
"CWE-348": "Use of Less Trusted Source",
"CWE-349": "Acceptance of Extraneous Untrusted Data With Trusted Data",
"CWE-35": "Path Traversal: '.../...//'",
"CWE-350": "Reliance on Reverse DNS Resolution for a Security-Critical Action",
"CWE-351": "Insufficient Type Distinction",
"CWE-352": "Cross-Site Request Forgery (CSRF)",
"CWE-353": "Missing Support for Integrity Check",
"CWE-354": "Improper Validation of Integrity Check Value",
"CWE-356": "Product UI does not Warn User of Unsafe Actions",
"CWE-357": "Insufficient UI Warning of Dangerous Operations",
"CWE-358": "Improperly Implemented Security Check for Standard",
"CWE-359": "Exposure of Private Personal Information to an Unauthorized Actor",
"CWE-36": "Absolute Path Traversal",
"CWE-360": "Trust of System Event Data",
"CWE-362": "Concurrent Execution using Shared Resource with Improper Synchronization ('Race Condition')",
"CWE-363": "Race Condition Enabling Link Following",
"CWE-364": "Signal Handler Race Condition",
"CWE-365": "DEPRECATED: Race Condition in Switch",
"CWE-366": "Race Condition within a Thread",
"CWE-367": "Time-of-check Time-of-use (TOCTOU) Race Condition",
"CWE-368": "Context Switching Race Condition",
"CWE-369": "Divide By Zero",
"CWE-37": "Path Traversal: '/absolute/pathname/here'",
"CWE-370": "Missing Check for Certificate Revocation after Initial Check",
"CWE-372": "Incomplete Internal State Distinction",
"CWE-373": "DEPRECATED: State Synchronization Error",
"CWE-374": "Passing Mutable Objects to an Untrusted Method",
"CWE-375": "Returning a Mutable Object to an Untrusted Caller",
"CWE-377": "Insecure Temporary File",
"CWE-378": "Creation of Temporary File With Insecure Permissions",
"CWE-379": "Creation of Temporary File in Directory with Insecure Permissions",
"CWE-38": "Path Traversal: '\\absolute\\pathname\\here'",
"CWE-382": "J2EE Bad Practices: Use of System.exit()",
"CWE-383": "J2EE Bad Practices: Direct Use of Threads",
"CWE-384": "Session Fixation",
"CWE-385": "Covert Timing Channel",
"CWE-386": "Symbolic Name not Mapping to Correct Object",
"CWE-39": "Path Traversal: 'C:dirname'",
"CWE-390": "Detection of Error Condition Without Action",
"CWE-391": "Unchecked Error Condition",
"CWE-392": "Missing Report of Error Condition",
"CWE-393": "Return of Wrong Status Code",
"CWE-394": "Unexpected Status Code or Return Value",
"CWE-395": "Use of NullPointerException Catch to Detect NULL Pointer Dereference",
"CWE-396": "Declaration of Catch for Generic Exception",
"CWE-397": "Declaration of Throws for Generic Exception",
"CWE-40": "Path Traversal: '\\\\UNC\\share\\name\\' (Windows UNC Share)",
"CWE-400": "Uncontrolled Resource Consumption",
"CWE-401": "Missing Release of Memory after Effective Lifetime",
"CWE-402": "Transmission of Private Resources into a New Sphere ('Resource Leak')",
"CWE-403": "Exposure of File Descriptor to Unintended Control Sphere ('File Descriptor Leak')",
"CWE-404": "Improper Resource Shutdown or Release",
"CWE-405": "Asymmetric Resource Consumption (Amplification)",
"CWE-406": "Insufficient Control of Network Message Volume (Network Amplification)",
"CWE-407": "Inefficient Algorithmic Complexity",
"CWE-408": "Incorrect Behavior Order: Early Amplification",
"CWE-409": "Improper Handling of Highly Compressed Data (Data Amplification)",
"CWE-41": "Improper Resolution of Path Equivalence",
"CWE-410": "Insufficient Resource Pool",
"CWE-412": "Unrestricted Externally Accessible Lock",
"CWE-413": "Improper Resource Locking",
"CWE-414": "Missing Lock Check",
"CWE-415": "Double Free",
"CWE-416": "Use After Free",
"CWE-419": "Unprotected Primary Channel",
"CWE-42": "Path Equivalence: 'filename.' (Trailing Dot)",
"CWE-420": "Unprotected Alternate Channel",
"CWE-421": "Race Condition During Access to Alternate Channel",
"CWE-422": "Unprotected Windows Messaging Channel ('Shatter')",
"CWE-423": "DEPRECATED: Proxied Trusted Channel",
"CWE-424": "Improper Protection of Alternate Path",
"CWE-425": "Direct Request ('Forced Browsing')",
"CWE-426": "Untrusted Search Path",
"CWE-427": "Uncontrolled Search Path Element",
"CWE-428": "Unquoted Search Path or Element",
"CWE-43": "Path Equivalence: 'filename....' (Multiple Trailing Dot)",
"CWE-430": "Deployment of Wrong Handler",
"CWE-431": "Missing Handler",
"CWE-432": "Dangerous Signal Handler not Disabled During Sensitive Operations",
"CWE-433": "Unparsed Raw Web Content Delivery",
"CWE-434": "Unrestricted Upload of File with Dangerous Type",
"CWE-435": "Improper Interaction Between Multiple Correctly-Behaving Entities",
"CWE-436": "Interpretation Conflict",
"CWE-437": "Incomplete Model of Endpoint Features",
"CWE-439": "Behavioral Change in New Version or Environment",
"CWE-44": "Path Equivalence: 'file.name' (Internal Dot)",
"CWE-440": "Expected Behavior Violation",
"CWE-441": "Unintended Proxy or Intermediary ('Confused Deputy')",
"CWE-443": "DEPRECATED: HTTP response splitting",
"CWE-444": "Inconsistent Interpretation of HTTP Requests ('HTTP Request/Response Smuggling')",
"CWE-446": "UI Discrepancy for Security Feature",
"CWE-447": "Unimplemented or Unsupported Feature in UI",
"CWE-448": "Obsolete Feature in UI",
"CWE-449": "The UI Performs the Wrong Action",
"CWE-45": "Path Equivalence: 'file...name' (Multiple Internal Dot)",
"CWE-450": "Multiple Interpretations of UI Input",
"CWE-451": "User Interface (UI) Misrepresentation of Critical Information",
"CWE-453": "Insecure Default Variable Initialization",
"CWE-454": "External Initialization of Trusted Variables or Data Stores",
"CWE-455": "Non-exit on Failed Initialization",
"CWE-456": "Missing Initialization of a Variable",
"CWE-457": "Use of Uninitialized Variable",
"CWE-458": "DEPRECATED: Incorrect Initialization",
"CWE-459": "Incomplete Cleanup",
"CWE-46": "Path Equivalence: 'filename ' (Trailing Space)",
"CWE-460": "Improper Cleanup on Thrown Exception",
"CWE-462": "Duplicate Key in Associative List (Alist)",
"CWE-463": "Deletion of Data Structure Sentinel",
"CWE-464": "Addition of Data Structure Sentinel",
"CWE-466": "Return of Pointer Value Outside of Expected Range",
"CWE-467": "Use of sizeof() on a Pointer Type",
"CWE-468": "Incorrect Pointer Scaling",
"CWE-469": "Use of Pointer Subtraction to Determine Size",
"CWE-47": "Path Equivalence: ' filename' (Leading Space)",
"CWE-470": "Use of Externally-Controlled Input to Select Classes or Code ('Unsafe Reflection')",
"CWE-471": "Modification of Assumed-Immutable Data (MAID)",
"CWE-472": "External Control of Assumed-Immutable Web Parameter",
"CWE-473": "PHP External Variable Modification",
"CWE-474": "Use of Function with Inconsistent Implementations",
"CWE-475": "Undefined Behavior for Input to API",
"CWE-476": "NULL Pointer Dereference",
"CWE-477": "Use of Obsolete Function",
"CWE-478": "Missing Default Case in Multiple Condition Expression",
"CWE-479": "Signal Handler Use of a Non-reentrant Function",
"CWE-48": "Path Equivalence: 'file name' (Internal Whitespace)",
"CWE-480": "Use of Incorrect Operator",
"CWE-481": "Assigning instead of Comparing",
"CWE-482": "Comparing instead of Assigning",
"CWE-483": "Incorrect Block Delimitation",
"CWE-484": "Omitted Break Statement in Switch",
"CWE-486": "Comparison of Classes by Name",
"CWE-487": "Reliance on Package-level Scope",
"CWE-488": "Exposure of Data Element to Wrong Session",
"CWE-489": "Active Debug Code",
"CWE-49": "Path Equivalence: 'filename/' (Trailing Slash)",
"CWE-491": "Public cloneable() Method Without Final ('Object Hijack')",
"CWE-492": "Use of Inner Class Containing Sensitive Data",
"CWE-493": "Critical Public Variable Without Final Modifier",
"CWE-494": "Download of Code Without Integrity Check",
"CWE-495": "Private Data Structure Returned From A Public Method",
"CWE-496": "Public Data Assigned to Private Array-Typed Field",
"CWE-497": "Exposure of Sensitive System Information to an Unauthorized Control Sphere",
"CWE-498": "Cloneable Class Containing Sensitive Information",
"CWE-499": "Serializable Class Containing Sensitive Data",
"CWE-5": "J2EE Misconfiguration: Data Transmission Without Encryption",
"CWE-50": "Path Equivalence: '//multiple/leading/slash'",
"CWE-500": "Public Static Field Not Marked Final",
"CWE-501": "Trust Boundary Violation",
"CWE-502": "Deserialization of Untrusted Data",
"CWE-506": "Embedded Malicious Code",
"CWE-507": "Trojan Horse",
"CWE-508": "Non-Replicating Malicious Code",
"CWE-509": "Replicating Malicious Code (Virus or Worm)",
"CWE-51": "Path Equivalence: '/multiple//internal/slash'",
"CWE-510": "Trapdoor",
"CWE-511": "Logic/Time Bomb",
"CWE-512": "Spyware",
"CWE-514": "Covert Channel",
"CWE-515": "Covert Storage Channel",
"CWE-516": "DEPRECATED: Covert Timing Channel",
"CWE-52": "Path Equivalence: '/multiple/trailing/slash//'",
"CWE-520": ".NET Misconfiguration: Use of Impersonation",
"CWE-521": "Weak Password Requirements",
"CWE-522": "Insufficiently Protected Credentials",
"CWE-523": "Unprotected Transport of Credentials",
"CWE-524": "Use of Cache Containing Sensitive Information",
"CWE-525": "Use of Web Browser Cache Containing Sensitive Information",
"CWE-526": "Cleartext Storage of Sensitive Information in an Environment Variable",
"CWE-527": "Exposure of Version-Control Repository to an Unauthorized Control Sphere",
"CWE-528": "Exposure of Core Dump File to an Unauthorized Control Sphere",
"CWE-529": "Exposure of Access Control List Files to an Unauthorized Control Sphere",
"CWE-53": "Path Equivalence: '\\multiple\\\\internal\\backslash'",
"CWE-530": "Exposure of Backup File to an Unauthorized Control Sphere",
"CWE-531": "Inclusion of Sensitive Information in Test Code",
"CWE-532": "Insertion of Sensitive Information into Log File",
"CWE-533": "DEPRECATED: Information Exposure Through Server Log Files",
"CWE-534": "DEPRECATED: Information Exposure Through Debug Log Files",
"CWE-535": "Exposure of Information Through Shell Error Message",
"CWE-536": "Servlet Runtime Error Message Containing Sensitive Information",
"CWE-537": "Java Runtime Error Message Containing Sensitive Information",
"CWE-538": "Insertion of Sensitive Information into Externally-Accessible File or Directory",
"CWE-539": "Use of Persistent Cookies Containing Sensitive Information",
"CWE-54": "Path Equivalence: 'filedir\\' (Trailing Backslash)",
"CWE-540": "Inclusion of Sensitive Information in Source Code",
"CWE-541": "Inclusion of Sensitive Information in an Include File",
"CWE-542": "DEPRECATED: Information Exposure Through Cleanup Log Files",
"CWE-543": "Use of Singleton Pattern Without Synchronization in a Multithreaded Context",
"CWE-544": "Missing Standardized Error Handling Mechanism",
"CWE-545": "DEPRECATED: Use of Dynamic Class Loading",
"CWE-546": "Suspicious Comment",
"CWE-547": "Use of Hard-coded, Security-relevant Constants",
"CWE-548": "Exposure of Information Through Directory Listing",
"CWE-549": "Missing Password Field Masking",
"CWE-55": "Path Equivalence: '/./' (Single Dot Directory)",
"CWE-550": "Server-generated Error Message Containing Sensitive Information",
"CWE-551": "Incorrect Behavior Order: Authorization Before Parsing and Canonicalization",
"CWE-552": "Files or Directories Accessible to External Parties",
"CWE-553": "Command Shell in Externally Accessible Directory",
"CWE-554": "ASP.NET Misconfiguration: Not Using Input Validation Framework",
"CWE-555": "J2EE Misconfiguration: Plaintext Password in Configuration File",
"CWE-556": "ASP.NET Misconfiguration: Use of Identity Impersonation",
"CWE-558": "Use of getlogin() in Multithreaded Application",
"CWE-56": "Path Equivalence: 'filedir*' (Wildcard)",
"CWE-560": "Use of umask() with chmod-style Argument",
"CWE-561": "Dead Code",
"CWE-562": "Return of Stack Variable Address",
"CWE-563": "Assignment to Variable without Use",
"CWE-564": "SQL Injection: Hibernate",
"CWE-565": "Reliance on Cookies without Validation and Integrity Checking",
"CWE-566": "Authorization Bypass Through User-Controlled SQL Primary Key",
"CWE-567": "Unsynchronized Access to Shared Data in a Multithreaded Context",
"CWE-568": "finalize() Method Without super.finalize()",
"CWE-57": "Path Equivalence: 'fakedir/../realdir/filename'",
"CWE-570": "Expression is Always False",
"CWE-571": "Expression is Always True",
"CWE-572": "Call to Thread run() instead of start()",
"CWE-573": "Improper Following of Specification by Caller",
"CWE-574": "EJB Bad Practices: Use of Synchronization Primitives",
"CWE-575": "EJB Bad Practices: Use of AWT Swing",
"CWE-576": "EJB Bad Practices: Use of Java I/O",
"CWE-577": "EJB Bad Practices: Use of Sockets",
"CWE-578": "EJB Bad Practices: Use of Class Loader",
"CWE-579": "J2EE Bad Practices: Non-serializable Object Stored in Session",
"CWE-58": "Path Equivalence: Windows 8.3 Filename",
"CWE-580": "clone() Method Without super.clone()",
"CWE-581": "Object Model Violation: Just One of Equals and Hashcode Defined",
"CWE-582": "Array Declared Public, Final, and Static",
"CWE-583": "finalize() Method Declared Public",
"CWE-584": "Return Inside Finally Block",
"CWE-585": "Empty Synchronized Block",
"CWE-586": "Explicit Call to Finalize()",
"CWE-587": "Assignment of a Fixed Address to a Pointer",
"CWE-588": "Attempt to Access Child of a Non-structure Pointer",
"CWE-589": "Call to Non-ubiquitous API",
"CWE-59": "Improper Link Resolution Before File Access ('Link Following')",
"CWE-590": "Free of Memory not on the Heap",
"CWE-591": "Sensitive Data Storage in Improperly Locked Memory",
"CWE-592": "DEPRECATED: Authentication Bypass Issues",
"CWE-593": "Authentication Bypass: OpenSSL CTX Object Modified after SSL Objects are Created",
"CWE-594": "J2EE Framework: Saving Unserializable Objects to Disk",
"CWE-595": "Comparison of Object References Instead of Object Contents",
"CWE-596": "DEPRECATED: Incorrect Semantic Object Comparison",
"CWE-597": "Use of Wrong Operator in String Comparison",
"CWE-598": "Use of GET Request Method With Sensitive Query Strings",
"CWE-599": "Missing Validation of OpenSSL Certificate",
"CWE-6": "J2EE Misconfiguration: Insufficient Session-ID Length",
"CWE-600": "Uncaught Exception in Servlet",
"CWE-601": "URL Redirection to Untrusted Site ('Open Redirect')",
"CWE-602": "Client-Side Enforcement of Server-Side Security",
"CWE-603": "Use of Client-Side Authentication",
"CWE-605": "Multiple Binds to the Same Port",
"CWE-606": "Unchecked Input for Loop Condition",
"CWE-607": "Public Static Final Field References Mutable Object",
"CWE-608": "Struts: Non-private Field in ActionForm Class",
"CWE-609": "Double-Checked Locking",
"CWE-61": "UNIX Symbolic Link (Symlink) Following",
"CWE-610": "Externally Controlled Reference to a Resource in Another Sphere",
"CWE-611": "Improper Restriction of XML External Entity Reference",
"CWE-612": "Improper Authorization of Index Containing Sensitive Information",
"CWE-613": "Insufficient Session Expiration",
"CWE-614": "Sensitive Cookie in HTTPS Session Without 'Secure' Attribute",
"CWE-615": "Inclusion of Sensitive Information in Source Code Comments",
"CWE-616": "Incomplete Identification of Uploaded File Variables (PHP)",
"CWE-617": "Reachable Assertion",
"CWE-618": "Exposed Unsafe ActiveX Method",
"CWE-619": "Dangling Database Cursor ('Cursor Injection')",
"CWE-62": "UNIX Hard Link",
"CWE-620": "Unverified Password Change",
"CWE-621": "Variable Extraction Error",
"CWE-622": "Improper Validation of Function Hook Arguments",
"CWE-623": "Unsafe ActiveX Control Marked Safe For Scripting",
"CWE-624": "Executable Regular Expression Error",
"CWE-625": "Permissive Regular Expression",
"CWE-626": "Null Byte Interaction Error (Poison Null Byte)",
"CWE-627": "Dynamic Variable Evaluation",
"CWE-628": "Function Call with Incorrectly Specified Arguments",
"CWE-636": "Not Failing Securely ('Failing Open')",
"CWE-637": "Unnecessary Complexity in Protection Mechanism (Not Using 'Economy of Mechanism')",
"CWE-638": "Not Using Complete Mediation",
"CWE-639": "Authorization Bypass Through User-Controlled Key",
"CWE-64": "Windows Shortcut Following (.LNK)",
"CWE-640": "Weak Password Recovery Mechanism for Forgotten Password",
"CWE-641": "Improper Restriction of Names for Files and Other Resources",
"CWE-642": "External Control of Critical State Data",
"CWE-643": "Improper Neutralization of Data within XPath Expressions ('XPath Injection')",
"CWE-644": "Improper Neutralization of HTTP Headers for Scripting Syntax",
"CWE-645": "Overly Restrictive Account Lockout Mechanism",
"CWE-646": "Reliance on File Name or Extension of Externally-Supplied File",
"CWE-647": "Use of Non-Canonical URL Paths for Authorization Decisions",
"CWE-648": "Incorrect Use of Privileged APIs",
"CWE-649": "Reliance on Obfuscation or Encryption of Security-Relevant Inputs without Integrity Checking",
"CWE-65": "Windows Hard Link",
"CWE-650": "Trusting HTTP Permission Methods on the Server Side",
"CWE-651": "Exposure of WSDL File Containing Sensitive Information",
"CWE-652": "Improper Neutralization of Data within XQuery Expressions ('XQuery Injection')",
"CWE-653": "Improper Isolation or Compartmentalization",
"CWE-654": "Reliance on a Single Factor in a Security Decision",
"CWE-655": "Insufficient Psychological Acceptability",
"CWE-656": "Reliance on Security Through Obscurity",
"CWE-657": "Violation of Secure Design Principles",
"CWE-66": "Improper Handling of File Names that Identify Virtual Resources",
"CWE-662": "Improper Synchronization",
"CWE-663": "Use of a Non-reentrant Function in a Concurrent Context",
"CWE-664": "Improper Control of a Resource Through its Lifetime",
"CWE-665": "Improper Initialization",
"CWE-666": "Operation on Resource in Wrong Phase of Lifetime",
"CWE-667": "Improper Locking",
"CWE-668": "Exposure of Resource to Wrong Sphere",
"CWE-669": "Incorrect Resource Transfer Between Spheres",
"CWE-67": "Improper Handling of Windows Device Names",
"CWE-670": "Always-Incorrect Control Flow Implementation",
"CWE-671": "Lack of Administrator Control over Security",
"CWE-672": "Operation on a Resource after Expiration or Release",
"CWE-673": "External Influence of Sphere Definition",
"CWE-674": "Uncontrolled Recursion",
"CWE-675": "Multiple Operations on Resource in Single-Operation Context",
"CWE-676": "Use of Potentially Dangerous Function",
"CWE-680": "Integer Overflow to Buffer Overflow",
"CWE-681": "Incorrect Conversion between Numeric Types",
"CWE-682": "Incorrect Calculation",
"CWE-683": "Function Call With Incorrect Order of Arguments",
"CWE-684": "Incorrect Provision of Specified Functionality",
"CWE-685": "Function Call With Incorrect Number of Arguments",
"CWE-686": "Function Call With Incorrect Argument Type",
"CWE-687": "Function Call With Incorrectly Specified Argument Value",
"CWE-688": "Function Call With Incorrect Variable or Reference as Argument",
"CWE-689": "Permission Race Condition During Resource Copy",
"CWE-69": "Improper Handling of Windows ::DATA Alternate Data Stream",
"CWE-690": "Unchecked Return Value to NULL Pointer Dereference",
"CWE-691": "Insufficient Control Flow Management",
"CWE-692": "Incomplete Denylist to Cross-Site Scripting",
"CWE-693": "Protection Mechanism Failure",
"CWE-694": "Use of Multiple Resources with Duplicate Identifier",
"CWE-695": "Use of Low-Level Functionality",
"CWE-696": "Incorrect Behavior Order",
"CWE-697": "Incorrect Comparison",
"CWE-698": "Execution After Redirect (EAR)",
"CWE-7": "J2EE Misconfiguration: Missing Custom Error Page",
"CWE-703": "Improper Check or Handling of Exceptional Conditions",
"CWE-704": "Incorrect Type Conversion or Cast",
"CWE-705": "Incorrect Control Flow Scoping",
"CWE-706": "Use of Incorrectly-Resolved Name or Reference",
"CWE-707": "Improper Neutralization",
"CWE-708": "Incorrect Ownership Assignment",
"CWE-71": "DEPRECATED: Apple '.DS_Store'",
"CWE-710": "Improper Adherence to Coding Standards",
"CWE-72": "Improper Handling of Apple HFS+ Alternate Data Stream Path",
"CWE-73": "External Control of File Name or Path",
"CWE-732": "Incorrect Permission Assignment for Critical Resource",
"CWE-733": "Compiler Optimization Removal or Modification of Security-critical Code",
"CWE-74": "Improper Neutralization of Special Elements in Output Used by a Downstream Component ('Injection')",
"CWE-749": "Exposed Dangerous Method or Function",
"CWE-75": "Failure to Sanitize Special Elements into a Different Plane (Special Element Injection)",
"CWE-754": "Improper Check for Unusual or Exceptional Conditions",
"CWE-755": "Improper Handling of Exceptional Conditions",
"CWE-756": "Missing Custom Error Page",
"CWE-757": "Selection of Less-Secure Algorithm During Negotiation ('Algorithm Downgrade')",
"CWE-758": "Reliance on Undefined, Unspecified, or Implementation-Defined Behavior",
"CWE-759": "Use of a One-Way Hash without a Salt",
"CWE-76": "Improper Neutralization of Equivalent Special Elements",
"CWE-760": "Use of a One-Way Hash with a Predictable Salt",
"CWE-761": "Free of Pointer not at Start of Buffer",
"CWE-762": "Mismatched Memory Management Routines",
"CWE-763": "Release of Invalid Pointer or Reference",
"CWE-764": "Multiple Locks of a Critical Resource",
"CWE-765": "Multiple Unlocks of a Critical Resource",
"CWE-766": "Critical Data Element Declared Public",
"CWE-767": "Access to Critical Private Variable via Public Method",
"CWE-768": "Incorrect Short Circuit Evaluation",
"CWE-769": "DEPRECATED: Uncontrolled File Descriptor Consumption",
"CWE-77": "Improper Neutralization of Special Elements used in a Command ('Command Injection')",
"CWE-770": "Allocation of Resources Without Limits or Throttling",
"CWE-771": "Missing Reference to Active Allocated Resource",
"CWE-772": "Missing Release of Resource after Effective Lifetime",
"CWE-773": "Missing Reference to Active File Descriptor or Handle",
"CWE-774": "Allocation of File Descriptors or Handles Without Limits or Throttling",
"CWE-775": "Missing Release of File Descriptor or Handle after Effective Lifetime",
"CWE-776": "Improper Restriction of Recursive Entity References in DTDs ('XML Entity Expansion')",
"CWE-777": "Regular Expression without Anchors",
"CWE-778": "Insufficient Logging",
"CWE-779": "Logging of Excessive Data",
"CWE-78": "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')",
"CWE-780": "Use of RSA Algorithm without OAEP",
"CWE-781": "Improper Address Validation in IOCTL with METHOD_NEITHER I/O Control Code",
"CWE-782": "Exposed IOCTL with Insufficient Access Control",
"CWE-783": "Operator Precedence Logic Error",
"CWE-784": "Reliance on Cookies without Validation and Integrity Checking in a Security Decision",
"CWE-785": "Use of Path Manipulation Function without Maximum-sized Buffer",
"CWE-786": "Access of Memory Location Before Start of Buffer",
"CWE-787": "Out-of-bounds Write",
"CWE-788": "Access of Memory Location After End of Buffer",
"CWE-789": "Memory Allocation with Excessive Size Value",
"CWE-79": "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')",
"CWE-790": "Improper Filtering of Special Elements",
"CWE-791": "Incomplete Filtering of Special Elements",
"CWE-792": "Incomplete Filtering of One or More Instances of Special Elements",
"CWE-793": "Only Filtering One Instance of a Special Element",
"CWE-794": "Incomplete Filtering of Multiple Instances of Special Elements",
"CWE-795": "Only Filtering Special Elements at a Specified Location",
"CWE-796": "Only Filtering Special Elements Relative to a Marker",
"CWE-797": "Only Filtering Special Elements at an Absolute Position",
"CWE-798": "Use of Hard-coded Credentials",
"CWE-799": "Improper Control of Interaction Frequency",
"CWE-8": "J2EE Misconfiguration: Entity Bean Declared Remote",
"CWE-80": "Improper Neutralization of Script-Related HTML Tags in a Web Page (Basic XSS)",
"CWE-804": "Guessable CAPTCHA",
"CWE-805": "Buffer Access with Incorrect Length Value",
"CWE-806": "Buffer Access Using Size of Source Buffer",
"CWE-807": "Reliance on Untrusted Inputs in a Security Decision",
"CWE-81": "Improper Neutralization of Script in an Error Message Web Page",
"CWE-82": "Improper Neutralization of Script in Attributes of IMG Tags in a Web Page",
"CWE-820": "Missing Synchronization",
"CWE-821": "Incorrect Synchronization",
"CWE-822": "Untrusted Pointer Dereference",
"CWE-823": "Use of Out-of-range Pointer Offset",
"CWE-824": "Access of Uninitialized Pointer",
"CWE-825": "Expired Pointer Dereference",
"CWE-826": "Premature Release of Resource During Expected Lifetime",
"CWE-827": "Improper Control of Document Type Definition",
"CWE-828": "Signal Handler with Functionality that is not Asynchronous-Safe",
"CWE-829": "Inclusion of Functionality from Untrusted Control Sphere",
"CWE-83": "Improper Neutralization of Script in Attributes in a Web Page",
"CWE-830": "Inclusion of Web Functionality from an Untrusted Source",
"CWE-831": "Signal Handler Function Associated with Multiple Signals",
"CWE-832": "Unlock of a Resource that is not Locked",
"CWE-833": "Deadlock",
"CWE-834": "Excessive Iteration",
"CWE-835": "Loop with Unreachable Exit Condition ('Infinite Loop')",
"CWE-836": "Use of Password Hash Instead of Password for Authentication",
"CWE-837": "Improper Enforcement of a Single, Unique Action",
"CWE-838": "Inappropriate Encoding for Output Context",
"CWE-839": "Numeric Range Comparison Without Minimum Check",
"CWE-84": "Improper Neutralization of Encoded URI Schemes in a Web Page",
"CWE-841": "Improper Enforcement of Behavioral Workflow",
"CWE-842": "Placement of User into Incorrect Group",
"CWE-843": "Access of Resource Using Incompatible Type ('Type Confusion')",
"CWE-85": "Doubled Character XSS Manipulations",
"CWE-86": "Improper Neutralization of Invalid Characters in Identifiers in Web Pages",
"CWE-862": "Missing Authorization",
"CWE-863": "Incorrect Authorization",
"CWE-87": "Improper Neutralization of Alternate XSS Syntax",
"CWE-88": "Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')",
"CWE-89": "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')",
"CWE-9": "J2EE Misconfiguration: Weak Access Permissions for EJB Methods",
"CWE-90": "Improper Neutralization of Special Elements used in an LDAP Query ('LDAP Injection')",
"CWE-908": "Use of Uninitialized Resource",
"CWE-909": "Missing Initialization of Resource",
"CWE-91": "XML Injection (aka Blind XPath Injection)",
"CWE-910": "Use of Expired File Descriptor",
"CWE-911": "Improper Update of Reference Count",
"CWE-912": "Hidden Functionality",
"CWE-913": "Improper Control of Dynamically-Managed Code Resources",
"CWE-914": "Improper Control of Dynamically-Identified Variables",
"CWE-915": "Improperly Controlled Modification of Dynamically-Determined Object Attributes",
"CWE-916": "Use of Password Hash With Insufficient Computational Effort",
"CWE-917": "Improper Neutralization of Special Elements used in an Expression Language Statement ('Expression Language Injection')",
"CWE-918": "Server-Side Request Forgery (SSRF)",
"CWE-92": "DEPRECATED: Improper Sanitization of Custom Special Characters",
"CWE-920": "Improper Restriction of Power Consumption",
"CWE-921": "Storage of Sensitive Data in a Mechanism without Access Control",
"CWE-922": "Insecure Storage of Sensitive Information",
"CWE-923": "Improper Restriction of Communication Channel to Intended Endpoints",
"CWE-924": "Improper Enforcement of Message Integrity During Transmission in a Communication Channel",
"CWE-925": "Improper Verification of Intent by Broadcast Receiver",
"CWE-926": "Improper Export of Android Application Components",
"CWE-927": "Use of Implicit Intent for Sensitive Communication",
"CWE-93": "Improper Neutralization of CRLF Sequences ('CRLF Injection')",
"CWE-939": "Improper Authorization in Handler for Custom URL Scheme",
"CWE-94": "Improper Control of Generation of Code ('Code Injection')",
"CWE-940": "Improper Verification of Source of a Communication Channel",
"CWE-941": "Incorrectly Specified Destination in a Communication Channel",
"CWE-942": "Permissive Cross-domain Security Policy with Untrusted Domains",
"CWE-943": "Improper Neutralization of Special Elements in Data Query Logic",
"CWE-95": "Improper Neutralization of Directives in Dynamically Evaluated Code ('Eval Injection')",
"CWE-96": "Improper Neutralization of Directives in Statically Saved Code ('Static Code Injection')",
"CWE-97": "Improper Neutralization of Server-Side Includes (SSI) Within a Web Page",
"CWE-98": "Improper Control of Filename for Include/Require Statement in PHP Program ('PHP Remote File Inclusion')",
"CWE-99": "Improper Control of Resource Identifiers ('Resource Injection')"
}
+900
View File
@@ -0,0 +1,900 @@
#!/usr/bin/env python3
"""Generate CSAF 2.0 advisories and CycloneDX 1.6 VEX documents from wolfSSL
CVE Program records (CVE JSON 5.x).
The CVE record (the authoritative artefact wolfSSL authors as a CNA, e.g. with
Vulnogram and published to cve.org / cvelistV5) supplies the structural facts:
CVE id, title, description, CWE, CVSS, affected/fixed version ranges,
references, credits, and dates.
What a CVE record does NOT carry in machine-readable form is the VEX
*determination* and the wolfSSL-specific product nuances. Those live in a
small per-CVE overlay (--vex-overlay):
* state / justification / response / detail -- the VEX determination.
* fixed_versions / remediation -- the mainline fix guidance.
* fips { ... } -- a separate FIPS product entry
(own module version + CMVP certificate, own status, own remediation):
FIPS customers cannot freely upgrade, and many CVEs fall outside the
FIPS module boundary, so FIPS is modelled as a distinct product.
* requires_defines / default_status -- a no-cost hedge: which build
flag gates the vulnerable code. Recorded as an informational note
only; this tool does NOT compute per-build reachability (that would be
a future, safety-critical "build-aware VEX" feature).
Granularity follows Red Hat's model: per-CVE is the default (the VEX
automation primitive); pass several records to emit one *bundled* per-release
CSAF advisory + one CycloneDX BOM carrying all vulnerabilities[].
This mirrors scripts/gen-sbom: pure stdlib, SOURCE_DATE_EPOCH-reproducible,
deterministic UUIDs, fail-rather-than-emit-garbage on malformed input.
"""
import argparse
import json
import os
import pathlib
import sys
import urllib.request
import uuid
from datetime import datetime, timezone
GEN_TOOL_NAME = 'wolfssl-advisory-gen'
GEN_TOOL_VERSION = '0.3'
_SCRIPTS_DIR = pathlib.Path(__file__).resolve().parent
_REPO_ROOT = _SCRIPTS_DIR.parent
# Canonical single-source-of-truth for wolfSSL advisories. `make advisory`
# and a bare `gen-advisory` invocation both default to these locations, so the
# tool and the build target are interchangeable. testdata/ is a *separate*,
# frozen copy used only by the test suite (see scripts/testdata/README.md).
DEFAULT_RECORDS_DIR = _REPO_ROOT / 'advisories' / 'records'
DEFAULT_OVERLAY = _REPO_ROOT / 'advisories' / 'vex-overlay.json'
DEFAULT_OUT_DIR = _REPO_ROOT / 'advisories' / 'out'
# Official CWE id -> name catalogue, shipped alongside this script. CSAF
# mandatory test 6.1.11 requires /vulnerabilities[]/cwe/name to be the *exact*
# MITRE name for the id, so we resolve the name from the catalogue rather than
# trusting the (often differently-cased) free text in the CVE record's
# problemType. Regenerate scripts/cwe-names.json from the official CWE list
# when MITRE publishes a new version.
_CWE_NAMES_PATH = _SCRIPTS_DIR / 'cwe-names.json'
def _load_cwe_names():
try:
with open(_CWE_NAMES_PATH) as f:
return json.load(f)
except (OSError, json.JSONDecodeError):
# Absence is non-fatal: gen-advisory simply omits cwe.name (and hence
# the whole cwe object) from CSAF, which keeps the output conformant.
return {}
CWE_NAMES = _load_cwe_names()
WOLFSSL_VENDOR = 'wolfSSL'
WOLFSSL_PUBLISHER = {
'category': 'vendor',
'name': 'wolfSSL Inc.',
'namespace': 'https://www.wolfssl.com',
}
WOLFSSL_ADVISORIES_URL = 'https://www.wolfssl.com/docs/security-vulnerabilities/'
# Canonical directory under which the generated CSAF JSON documents are
# published. CSAF 2.0 requires the document's `self` reference to point at the
# canonical location of *this* document (not a generic landing page), so the
# self URL is built as <base>/<tracking-id>.csaf.json, matching the on-disk
# filename this script writes.
#
# DEPLOY-TIME FOLLOW-UPS (not enforced by the CSAF mandatory-test gate in
# .github/workflows/advisory.yml, which validates document content, not the
# distribution layout):
# - Base URL: this is a placeholder and will 404 until the documents are
# actually hosted. Point it at wolfSSL's real CSAF distribution location.
# - Filename convention: CSAF's recommended filename is the tracking id
# lowercased with every char outside [+\-a-z0-9] replaced by '_' and a
# plain '.json' extension (e.g. cve-2026-5501.json). We keep the
# '<ID>.csaf.json' form here so the self URL stays consistent with the
# emitted file and so co-located '<ID>.cdx.json' VEX docs remain glob-
# selectable; switch both the self URL and the writer to the canonical
# lowercase name when the hosting layout is finalized.
WOLFSSL_CSAF_BASE_URL = WOLFSSL_ADVISORIES_URL + 'csaf/'
ADVISORY_UUID_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL,
'https://wolfssl.com/advisory/')
# Severity ordering for picking a bundle's aggregate_severity.
_SEV_RANK = {'CRITICAL': 4, 'HIGH': 3, 'MEDIUM': 2, 'MODERATE': 2, 'LOW': 1,
'NONE': 0}
# CycloneDX analysis.state -> CSAF product_status bucket for the *vulnerable*
# version ranges (those the CVE record marks status=affected). Every state in
# the CycloneDX 1.6 vocabulary (mirrored by the overlay schema) is mapped
# explicitly; _bucket_for() hard-fails on anything else rather than silently
# defaulting an unknown determination to the worst case (known_affected).
#
# 'resolved' / 'resolved_with_pedigree' map to known_affected on purpose: the
# ranges placed in this bucket are the vulnerable versions, which remain
# affected even after a fix ships. The fixed version is emitted separately
# into the 'fixed' bucket (see the per-product registration loop), so a
# resolved CVE correctly reports old ranges as known_affected and the patched
# release as fixed. Mapping 'resolved' to 'fixed' here would wrongly mark the
# vulnerable ranges as patched.
#
# not_affected / false_positive map to known_not_affected (and also emit a CSAF
# flag justification).
_STATE_TO_BUCKET = {
'exploitable': 'known_affected',
'in_triage': 'under_investigation',
'resolved': 'known_affected',
'resolved_with_pedigree': 'known_affected',
'false_positive': 'known_not_affected',
'not_affected': 'known_not_affected',
}
def _bucket_for(state):
"""Map a CycloneDX analysis state to its CSAF product_status bucket,
failing loudly on an unrecognized state instead of defaulting to the
worst-case (known_affected) bucket."""
try:
return _STATE_TO_BUCKET[state]
except KeyError:
sys.exit(f"ERROR: unknown analysis state {state!r}; expected one of "
f"{', '.join(sorted(_STATE_TO_BUCKET))}")
# CycloneDX not_affected justification -> CSAF flag label (same VEX concept).
_JUSTIFICATION_TO_CSAF_FLAG = {
'code_not_present': 'vulnerable_code_not_present',
'code_not_reachable': 'vulnerable_code_not_in_execute_path',
'requires_configuration': 'vulnerable_code_not_in_execute_path',
'requires_dependency': 'vulnerable_code_not_in_execute_path',
'requires_environment': 'vulnerable_code_not_in_execute_path',
'protected_by_compiler': 'inline_mitigations_already_exist',
'protected_at_perimeter': 'inline_mitigations_already_exist',
'protected_at_runtime': 'inline_mitigations_already_exist',
'protected_by_mitigating_control': 'inline_mitigations_already_exist',
}
def derived_uuid(*parts):
"""Deterministic UUID from joined parts (NUL-separated, no aliasing)."""
return str(uuid.uuid5(ADVISORY_UUID_NAMESPACE, '\x00'.join(parts)))
def build_timestamp():
"""(datetime, ISO-8601-Z) honoring SOURCE_DATE_EPOCH for reproducibility."""
sde = os.environ.get('SOURCE_DATE_EPOCH', '').strip()
if sde:
try:
dt = datetime.fromtimestamp(int(sde), tz=timezone.utc)
except (ValueError, OverflowError, OSError) as e:
print(f"WARNING: ignoring invalid SOURCE_DATE_EPOCH={sde!r}: {e}",
file=sys.stderr)
dt = datetime.now(timezone.utc)
else:
dt = datetime.now(timezone.utc)
return dt, dt.strftime('%Y-%m-%dT%H:%M:%SZ')
def cpe_for(product, version):
"""CPE 2.3 for a wolfSSL product at a version (matches gen-sbom)."""
return f'cpe:2.3:a:wolfssl:{product.lower()}:{version}:*:*:*:*:*:*:*'
def purl_for(product, version):
"""PURL for a wolfSSL product release (matches gen-sbom: pkg:github)."""
return f'pkg:github/wolfSSL/{product.lower()}@v{version}'
# --------------------------------------------------------------------------- #
# CVE record parsing
# --------------------------------------------------------------------------- #
def load_cve_record(path=None, cve_id=None):
if path:
try:
with open(path) as f:
return json.load(f)
except (OSError, json.JSONDecodeError) as e:
sys.exit(f"ERROR: cannot read CVE record {path!r}: {e}")
url = f'https://cveawg.mitre.org/api/cve/{cve_id}'
try:
with urllib.request.urlopen(url, timeout=30) as r:
return json.loads(r.read().decode())
except Exception as e: # noqa: BLE001 - surface any fetch/parse failure
sys.exit(f"ERROR: cannot fetch {url}: {e}")
def _cna(record):
try:
return record['containers']['cna']
except (KeyError, TypeError):
sys.exit("ERROR: CVE record has no containers.cna section")
def _best_cvss(metrics):
"""Pick the highest-priority CVSS block (v4 > v3.1 > v3.0 > v2).
Used for the CycloneDX rating (which supports v4) and for the document's
aggregate_severity."""
for key, ver, method in (
('cvssV4_0', 'v4', 'CVSSv4'),
('cvssV3_1', 'v3', 'CVSSv31'),
('cvssV3_0', 'v3', 'CVSSv3'),
('cvssV2_0', 'v2', 'CVSSv2'),
):
for m in metrics:
if key in m:
return {'data': m[key], 'csaf_key': f'cvss_{ver}',
'cdx_method': method}
return None
def _best_cvss_csaf20(metrics):
"""Pick the highest-priority CVSS block that CSAF 2.0 scores[] can carry.
The CSAF 2.0 schema predates CVSS v4 and only defines cvss_v2 / cvss_v3 in
a score object, so a v4 block must NOT be placed there (it fails the strict
schema). When only v4 exists the CSAF emitter records it as a note and
relies on the CycloneDX VEX output for the machine-readable v4 rating."""
for key, ver in (('cvssV3_1', 'v3'), ('cvssV3_0', 'v3'), ('cvssV2_0', 'v2')):
for m in metrics:
if key in m:
return {'data': m[key], 'csaf_key': f'cvss_{ver}'}
return None
def parse_record(record):
"""Reduce a CVE 5.x record to the fields the emitters need."""
meta = record.get('cveMetadata', {})
cna = _cna(record)
cve_id = meta.get('cveId') or cna.get('cveId')
if not cve_id:
sys.exit("ERROR: CVE record has no cveId")
description = ''
for d in cna.get('descriptions', []):
if d.get('lang', '').lower().startswith('en'):
description = d.get('value', '')
break
# CSAF 2.0 carries a single `cwe` {id, name}; take the primary one. The
# name is resolved from the official CWE catalogue (CWE_NAMES) so it is the
# exact MITRE string CSAF test 6.1.11 checks against -- the record's
# free-text problemType often differs in casing. `name` may be None when
# the id is absent from the catalogue; the CSAF emitter then omits cwe.
cwe = None
for pt in cna.get('problemTypes', []):
for d in pt.get('descriptions', []):
cid = d.get('cweId')
if not cid:
continue
cwe = {'id': cid, 'name': CWE_NAMES.get(cid)}
break
if cwe:
break
metrics = cna.get('metrics', [])
cvss = _best_cvss(metrics)
cvss_csaf = _best_cvss_csaf20(metrics)
affected = []
for a in cna.get('affected', []):
affected.append({
'vendor': a.get('vendor', WOLFSSL_VENDOR),
'product': a.get('product', 'wolfSSL'),
'versions': a.get('versions', []),
'default_status': a.get('defaultStatus', 'unknown'),
})
references = [r['url'] for r in cna.get('references', []) if r.get('url')]
credits = [c.get('value', '') for c in cna.get('credits', [])
if c.get('value')]
return {
'cve': cve_id,
'title': cna.get('title') or cve_id,
'description': description,
'cwe': cwe,
'cvss': cvss,
'cvss_csaf': cvss_csaf,
'affected': affected,
'references': references,
'credits': credits,
'date_published': meta.get('datePublished'),
'date_updated': meta.get('dateUpdated'),
}
def _range_label(v):
base = v.get('version', '0')
if v.get('lessThanOrEqual'):
return f'<= {v["lessThanOrEqual"]}' if base in ('0', '*') \
else f'{base} <= x <= {v["lessThanOrEqual"]}'
if v.get('lessThan'):
return f'< {v["lessThan"]}' if base in ('0', '*') \
else f'{base} <= x < {v["lessThan"]}'
return base
def _vers_range(v):
parts = []
base = v.get('version')
if base and base not in ('0', '*'):
parts.append(f'>={base}')
if v.get('lessThanOrEqual'):
parts.append(f'<={v["lessThanOrEqual"]}')
elif v.get('lessThan'):
parts.append(f'<{v["lessThan"]}')
elif base and base not in ('0', '*'):
return f'vers:generic/{base}'
return 'vers:generic/' + '|'.join(parts) if parts else 'vers:generic/*'
# --------------------------------------------------------------------------- #
# Product model: normalize record + overlay into a list of product entries.
# --------------------------------------------------------------------------- #
def product_model(adv, ov):
"""Return the products this CVE describes: the mainline wolfSSL product
(from the CVE record) plus an optional, separately-modelled FIPS product
(from the overlay)."""
products = []
state = ov.get('state', 'exploitable')
bucket = _bucket_for(state)
# ---- mainline product(s) from the CVE record's affected[] ----
for a in adv['affected']:
product = a['product']
affected_ranges = []
for v in a['versions']:
if v.get('status') == 'affected' or a['default_status'] == 'affected':
affected_ranges.append({
'label': _range_label(v),
'cpe': cpe_for(product, '*'),
'vers': _vers_range(v),
})
fixed = []
for fv in ov.get('fixed_versions', []):
fixed.append({'version': fv, 'cpe': cpe_for(product, fv),
'purl': purl_for(product, fv)})
remediation = ov.get('remediation')
if not remediation and fixed:
remediation = f'Update to {product} {fixed[0]["version"]} or later.'
products.append({
'product_name': product,
'cdx_key': product.lower(),
'bucket': bucket,
'justification': ov.get('justification') if bucket
== 'known_not_affected' else None,
'remediation': remediation,
'remediation_category': 'vendor_fix' if fixed else 'none_available',
'affected_ranges': affected_ranges,
'fixed': fixed,
'model_numbers': [],
})
# ---- optional FIPS product from the overlay ----
fips = ov.get('fips')
if fips:
name = fips.get('name', 'wolfCrypt FIPS Module')
modver = fips.get('module_version')
fbucket = _bucket_for(fips.get('status', 'exploitable'))
affected_ranges = []
if modver:
affected_ranges.append({
'label': modver,
'cpe': cpe_for('wolfcrypt', modver),
'vers': f'vers:generic/{modver}',
})
fixed = []
for fv in fips.get('fixed_versions', []):
fixed.append({'version': fv, 'cpe': cpe_for('wolfcrypt', fv),
'purl': purl_for('wolfcrypt', fv)})
model_numbers = []
if fips.get('cmvp_cert'):
model_numbers.append(f'CMVP Certificate #{fips["cmvp_cert"]}')
products.append({
'product_name': name,
'cdx_key': 'wolfcrypt-fips',
'bucket': fbucket,
'justification': fips.get('justification') if fbucket
== 'known_not_affected' else None,
'remediation': fips.get('remediation'),
'remediation_category': 'vendor_fix' if fixed else (
'no_fix_planned' if fbucket == 'known_not_affected'
else 'none_available'),
'affected_ranges': affected_ranges,
'fixed': fixed,
'model_numbers': model_numbers,
'module_version': modver,
})
return products
def _hedge_note(ov):
"""Render the no-cost reachability hedge as informational text (only)."""
bits = []
defines = ov.get('requires_defines')
if defines:
bits.append('Reachable only in builds compiled with: '
+ ', '.join(defines) + '.')
if ov.get('default_status') in ('off', 'disabled'):
bits.append('The affected feature is disabled by default.')
return ' '.join(bits) if bits else None
# --------------------------------------------------------------------------- #
# CSAF 2.0 emitter (handles 1..N vulnerabilities in one document)
# --------------------------------------------------------------------------- #
def generate_csaf(advs, ov_map, advisory_id, timestamp):
# Shared product_tree: product leaves are deduplicated by product_id across
# all CVEs in the bundle (Red Hat-style shared product ids).
tree = {} # (vendor, product_name) -> {product_id: leaf}
tree_order = [] # preserve insertion order of (vendor, product_name)
vulns = []
agg_rank = -1
agg_text = None
init_dates = []
cur_dates = []
def _leaf_range(pname, label, cpe, model_numbers):
pid = derived_uuid(pname, 'range', label)
helper = {'cpe': cpe}
if model_numbers:
helper['model_numbers'] = model_numbers
return pid, {
'category': 'product_version_range',
'name': label,
'product': {
'product_id': pid,
'name': f'{pname} {label}',
'product_identification_helper': helper,
},
}
def _leaf_fixed(pname, fx, model_numbers):
pid = derived_uuid(pname, 'fixed', fx['version'])
helper = {'cpe': fx['cpe'], 'purl': fx['purl']}
if model_numbers:
helper['model_numbers'] = model_numbers
return pid, {
'category': 'product_version',
'name': fx['version'],
'product': {
'product_id': pid,
'name': f'{pname} {fx["version"]}',
'product_identification_helper': helper,
},
}
def _register(pname, pid, leaf):
key = (WOLFSSL_VENDOR, pname)
if key not in tree:
tree[key] = {}
tree_order.append(key)
tree[key].setdefault(pid, leaf)
for adv in advs:
ov = ov_map.get(adv['cve'], {})
products = product_model(adv, ov)
status_buckets = {} # bucket -> [pids]
score_targets = []
flags = {} # flag_label -> [pids]
remediations = {} # (category, text, url) -> [pids]
for prod in products:
pname = prod['product_name']
affected_pids = []
for r in prod['affected_ranges']:
pid, leaf = _leaf_range(pname, r['label'], r['cpe'],
prod['model_numbers'])
_register(pname, pid, leaf)
affected_pids.append(pid)
status_buckets.setdefault(prod['bucket'], []).append(pid)
if prod['bucket'] in ('known_affected', 'under_investigation'):
score_targets.append(pid)
if prod['bucket'] == 'known_not_affected':
flag = _JUSTIFICATION_TO_CSAF_FLAG.get(
prod['justification'] or '',
'vulnerable_code_not_in_execute_path')
flags.setdefault(flag, []).append(pid)
for fx in prod['fixed']:
pid, leaf = _leaf_fixed(pname, fx, prod['model_numbers'])
_register(pname, pid, leaf)
status_buckets.setdefault('fixed', []).append(pid)
if prod['remediation'] and affected_pids:
url = adv['references'][0] if adv['references'] else None
key = (prod['remediation_category'], prod['remediation'], url)
remediations.setdefault(key, []).extend(affected_pids)
vuln = {
'cve': adv['cve'],
'notes': [{
'category': 'description',
'text': adv['description'],
'title': 'Vulnerability description',
}],
'product_status': {k: v for k, v in status_buckets.items() if v},
}
hedge = _hedge_note(ov)
if hedge:
vuln['notes'].append({
'category': 'other', 'title': 'Build reachability', 'text': hedge})
# Emit cwe only when we resolved the exact catalogue name (CSAF 6.1.11).
if adv['cwe'] and adv['cwe'].get('name'):
vuln['cwe'] = {'id': adv['cwe']['id'], 'name': adv['cwe']['name']}
if adv['references']:
vuln['references'] = [{'summary': u, 'url': u, 'category': 'external'}
for u in adv['references']]
if flags:
vuln['flags'] = [{'label': lbl, 'product_ids': pids}
for lbl, pids in flags.items()]
if remediations:
vuln['remediations'] = []
for (cat, text, url), pids in remediations.items():
rem = {'category': cat, 'details': text, 'product_ids': pids}
if url:
rem['url'] = url
vuln['remediations'].append(rem)
# CSAF 2.0 scores[] can only carry CVSS v2/v3 (the schema predates v4).
if adv['cvss_csaf'] and score_targets:
vuln['scores'] = [{adv['cvss_csaf']['csaf_key']:
adv['cvss_csaf']['data'],
'products': score_targets}]
# aggregate_severity uses the best CVSS available (which may be v4).
best = adv['cvss']
if best:
sev = best['data'].get('baseSeverity', '').upper()
if _SEV_RANK.get(sev, -1) > agg_rank:
agg_rank = _SEV_RANK[sev]
agg_text = best['data'].get('baseSeverity')
# A v4-only finding cannot be represented in CSAF 2.0 scores[];
# preserve the rating as a note so it is not silently dropped.
if best['csaf_key'] == 'cvss_v4' and not adv['cvss_csaf']:
v4 = best['data']
vuln['notes'].append({
'category': 'other',
'title': 'CVSS v4.0',
'text': (f"CVSS v4.0 base score {v4.get('baseScore')} "
f"({v4.get('baseSeverity')}); vector "
f"{v4.get('vectorString')}. CSAF 2.0 scores[] "
f"cannot encode CVSS v4; the machine-readable v4 "
f"rating is provided in the CycloneDX VEX output."),
})
if adv['credits']:
vuln['acknowledgments'] = [{'summary': c} for c in adv['credits']]
vulns.append(vuln)
if adv['date_published']:
init_dates.append(adv['date_published'])
cur_dates.append(adv['date_updated'] or adv['date_published'] or timestamp)
# Assemble product_tree branches.
branches = []
for (vendor, pname) in tree_order:
branches.append({
'category': 'vendor', 'name': vendor,
'branches': [{
'category': 'product_name', 'name': pname,
'branches': list(tree[(vendor, pname)].values()),
}],
})
bundle = len(advs) > 1
if bundle:
title = f'wolfSSL Security Advisory {advisory_id}'
else:
title = f'wolfSSL: {advs[0]["title"]}'
doc = {
'document': {
'category': 'csaf_security_advisory',
'csaf_version': '2.0',
'title': title,
'publisher': WOLFSSL_PUBLISHER,
'tracking': {
'id': advisory_id,
'status': 'final',
'version': '1',
'initial_release_date': min(init_dates) if init_dates
else timestamp,
'current_release_date': max(cur_dates) if cur_dates
else timestamp,
'revision_history': [{
'number': '1',
'date': min(init_dates) if init_dates else timestamp,
'summary': 'Initial release',
}],
'generator': {
'engine': {'name': GEN_TOOL_NAME, 'version': GEN_TOOL_VERSION},
},
},
'distribution': {'tlp': {'label': 'WHITE'}},
'references': [
{
'summary': f'Canonical CSAF document for {advisory_id}',
'url': f'{WOLFSSL_CSAF_BASE_URL}{advisory_id}.csaf.json',
'category': 'self',
},
{
'summary': 'wolfSSL published security vulnerabilities',
'url': WOLFSSL_ADVISORIES_URL,
'category': 'external',
},
],
'notes': [{
'category': 'summary',
'title': 'Summary',
'text': (f'wolfSSL security advisory {advisory_id} covering '
f'{len(advs)} vulnerabilities.' if bundle
else advs[0]['description']),
}],
},
'product_tree': {'branches': branches},
'vulnerabilities': vulns,
}
if agg_text:
doc['document']['aggregate_severity'] = {'text': agg_text}
return doc
# --------------------------------------------------------------------------- #
# CycloneDX 1.6 VEX emitter (handles 1..N vulnerabilities in one BOM)
# --------------------------------------------------------------------------- #
def _cdx_severity(cvss_data):
sev = (cvss_data or {}).get('baseSeverity', '').lower()
return sev if sev in ('critical', 'high', 'medium', 'low', 'none') \
else 'unknown'
def generate_cdx_vex(advs, ov_map, advisory_id, timestamp):
main_ref = derived_uuid('cdx-component', 'wolfssl')
extra_components = {} # ref -> component dict (e.g. FIPS module)
vulns = []
for adv in advs:
ov = ov_map.get(adv['cve'], {})
products = product_model(adv, ov)
affects = []
for prod in products:
if prod['cdx_key'] == 'wolfcrypt-fips':
ref = derived_uuid('cdx-component', 'wolfcrypt-fips',
prod.get('module_version') or '')
if ref not in extra_components:
comp = {
'bom-ref': ref, 'type': 'library',
'supplier': {'name': 'wolfSSL Inc.'},
'name': prod['product_name'],
'cpe': cpe_for('wolfcrypt', prod.get('module_version')
or '*'),
}
if prod.get('module_version'):
comp['version'] = prod['module_version']
if prod['model_numbers']:
comp['properties'] = [
{'name': 'wolfssl:fips:cmvp', 'value': m}
for m in prod['model_numbers']]
extra_components[ref] = comp
else:
ref = main_ref
# CycloneDX affects[].versions[].status uses 'unaffected' for a
# not-affected product; the 'not_affected' term belongs to
# analysis.state only.
astatus = 'unaffected' if prod['bucket'] == 'known_not_affected' \
else 'affected'
versions = [{'range': r['vers'], 'status': astatus}
for r in prod['affected_ranges']]
versions += [{'version': fx['version'], 'status': 'unaffected'}
for fx in prod['fixed']]
if versions:
affects.append({'ref': ref, 'versions': versions})
vuln = {
'id': adv['cve'],
'source': {'name': 'wolfSSL', 'url': WOLFSSL_ADVISORIES_URL},
'description': adv['description'],
'affects': affects or [{'ref': main_ref}],
}
if adv['cvss']:
rating = {'source': {'name': 'wolfSSL'},
'severity': _cdx_severity(adv['cvss']['data']),
'method': adv['cvss']['cdx_method']}
if 'baseScore' in adv['cvss']['data']:
rating['score'] = adv['cvss']['data']['baseScore']
if 'vectorString' in adv['cvss']['data']:
rating['vector'] = adv['cvss']['data']['vectorString']
vuln['ratings'] = [rating]
if adv['cwe']:
try:
vuln['cwes'] = [int(adv['cwe']['id'].split('-')[-1])]
except ValueError:
pass
if adv['references']:
vuln['advisories'] = [{'url': u} for u in adv['references']]
analysis = {'state': ov.get('state', 'exploitable')}
if analysis['state'] == 'not_affected' and ov.get('justification'):
analysis['justification'] = ov['justification']
if ov.get('response'):
analysis['response'] = ov['response']
detail_bits = [b for b in (ov.get('detail'), _hedge_note(ov)) if b]
if detail_bits:
analysis['detail'] = ' '.join(detail_bits)
vuln['analysis'] = analysis
if adv['credits']:
vuln['credits'] = {'individuals': [{'name': c}
for c in adv['credits']]}
if adv['date_published']:
vuln['published'] = adv['date_published']
if adv['date_updated']:
vuln['updated'] = adv['date_updated']
vulns.append(vuln)
return {
'$schema': 'http://cyclonedx.org/schema/bom-1.6.schema.json',
'bomFormat': 'CycloneDX',
'specVersion': '1.6',
'serialNumber': f'urn:uuid:{derived_uuid(advisory_id, "serial")}',
'version': 1,
'metadata': {
'timestamp': timestamp,
'tools': {'components': [{
'type': 'application', 'author': 'wolfSSL Inc.',
'name': GEN_TOOL_NAME, 'version': GEN_TOOL_VERSION}]},
'component': {
'bom-ref': main_ref, 'type': 'library',
'supplier': {'name': 'wolfSSL Inc.'},
'name': 'wolfssl',
'cpe': cpe_for('wolfssl', '*'),
'purl': 'pkg:github/wolfSSL/wolfssl',
},
},
'components': list(extra_components.values()),
'vulnerabilities': vulns,
}
# --------------------------------------------------------------------------- #
def load_overlay(path):
if not path:
return {}
try:
with open(path) as f:
return json.load(f)
except (OSError, json.JSONDecodeError) as e:
sys.exit(f"ERROR: cannot read --vex-overlay {path!r}: {e}")
def _write_json(obj, path):
try:
with open(path, 'w') as f:
json.dump(obj, f, indent=2)
f.write('\n')
except OSError as e:
sys.exit(f"ERROR: cannot write {path}: {e}")
def _emit(advs, ov_map, advisory_id, timestamp, csaf_out, cdx_out):
"""Emit one CSAF and/or one CycloneDX document covering `advs`."""
if csaf_out:
_write_json(generate_csaf(advs, ov_map, advisory_id, timestamp),
csaf_out)
print(f"Generated: {csaf_out}")
if cdx_out:
_write_json(generate_cdx_vex(advs, ov_map, advisory_id, timestamp),
cdx_out)
print(f"Generated: {cdx_out}")
def main():
p = argparse.ArgumentParser(
description='Generate CSAF 2.0 advisories and CycloneDX 1.6 VEX from '
'wolfSSL CVE Program records. With no record arguments it '
'processes every record in the canonical advisories/ tree '
'(the same inputs `make advisory` uses), writing one CSAF '
'+ one CycloneDX document per CVE into the output '
'directory.')
p.add_argument('--cve-record', action='append', default=[],
help='Path to a CVE JSON 5.x record (repeatable). Overrides '
'the default --records-dir scan.')
p.add_argument('--cve-id', action='append', default=[],
help='Fetch a record from cve.org by id (repeatable). '
'Overrides the default --records-dir scan.')
p.add_argument('--records-dir', default=str(DEFAULT_RECORDS_DIR),
help='Directory of CVE JSON 5.x records scanned when no '
f'--cve-record/--cve-id is given (default: '
f'{DEFAULT_RECORDS_DIR}).')
p.add_argument('--vex-overlay', default=None,
help='JSON file mapping CVE id -> VEX overlay (state, '
'justification, detail, response, fixed_versions, '
'remediation, fips{}, requires_defines, '
f'default_status). Default: {DEFAULT_OVERLAY} if it '
'exists.')
p.add_argument('--advisory-id',
help='Tracking id when bundling several records into ONE '
'document. Defaults to the CVE id for a single record.')
p.add_argument('--out-dir', default=str(DEFAULT_OUT_DIR),
help='Output directory for the per-CVE documents written in '
'batch mode (default: ' f'{DEFAULT_OUT_DIR}).')
p.add_argument('--csaf-out',
help='Write a single CSAF document to this path instead of '
'batch mode (one record, or several with '
'--advisory-id).')
p.add_argument('--cdx-vex-out',
help='Write a single CycloneDX VEX document to this path '
'instead of batch mode.')
args = p.parse_args()
# ---- resolve the input records ----
explicit = bool(args.cve_record or args.cve_id)
if explicit:
records = [load_cve_record(path=pth) for pth in args.cve_record]
records += [load_cve_record(cve_id=cid) for cid in args.cve_id]
else:
rec_dir = pathlib.Path(args.records_dir)
paths = sorted(rec_dir.glob('*.json'))
if not paths:
sys.exit(f"ERROR: no CVE records found in {rec_dir}. Add records "
f"there, or pass --cve-record / --cve-id.")
records = [load_cve_record(path=str(pth)) for pth in paths]
advs = [parse_record(r) for r in records]
# ---- resolve the overlay (explicit > default-if-present > none) ----
overlay_path = args.vex_overlay
if overlay_path is None and DEFAULT_OVERLAY.exists():
overlay_path = str(DEFAULT_OVERLAY)
ov_map = load_overlay(overlay_path)
if overlay_path:
for adv in advs:
if adv['cve'] not in ov_map:
print(f"WARNING: no VEX overlay entry for {adv['cve']}; "
f"defaulting to state=exploitable", file=sys.stderr)
_, timestamp = build_timestamp()
# ---- single-document mode (explicit output path) ----
if args.csaf_out or args.cdx_vex_out:
if args.advisory_id:
advisory_id = args.advisory_id
elif len(advs) == 1:
advisory_id = advs[0]['cve']
else:
sys.exit("ERROR: --advisory-id is required when bundling several "
"records into one --csaf-out/--cdx-vex-out document.")
_emit(advs, ov_map, advisory_id, timestamp,
args.csaf_out, args.cdx_vex_out)
return
# ---- batch mode: one CSAF + one CycloneDX per CVE into --out-dir ----
out_dir = pathlib.Path(args.out_dir)
try:
out_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
sys.exit(f"ERROR: cannot create output directory {out_dir}: {e}")
if args.advisory_id and len(advs) > 1:
_emit(advs, ov_map, args.advisory_id, timestamp,
str(out_dir / f'{args.advisory_id}.csaf.json'),
str(out_dir / f'{args.advisory_id}.cdx.json'))
else:
for adv in advs:
_emit([adv], ov_map, adv['cve'], timestamp,
str(out_dir / f"{adv['cve']}.csaf.json"),
str(out_dir / f"{adv['cve']}.cdx.json"))
print(f"Wrote {len(advs)} advisory document set(s) to {out_dir}")
if __name__ == '__main__':
main()
+1327
View File
File diff suppressed because it is too large Load Diff
+35
View File
@@ -170,3 +170,38 @@ EXTRA_DIST += scripts/bench/bench_functions.sh
EXTRA_DIST += scripts/benchmark_compare.sh
EXTRA_DIST += scripts/user_settings_asm.sh
# SBOM generator (invoked from `make sbom` in the top-level Makefile.am).
# Must be in the dist tarball, otherwise `make dist && cd <tarball> &&
# ./configure && make sbom` fails for downstream consumers.
EXTRA_DIST += scripts/gen-sbom
# SBOM generator unit tests. Shipped so downstream consumers building
# from a release tarball can re-run the regression suite.
EXTRA_DIST += scripts/test_gen_sbom.py
# Bomsh / OmniBOR provenance verifier (invoked from `.github/workflows/
# sbom.yml` and runnable by hand against any local `make bomsh` output;
# see doc/SBOM.md sec. 3.5). Must ship with the dist tarball so a
# downstream consumer / CRA reviewer who clones a release tarball can
# re-verify the OmniBOR graph against its enriched SPDX without going
# back to the git repo.
EXTRA_DIST += scripts/bomsh_verify.py
# Security advisory generator (invoked from `make advisory`), its canonical
# CWE-name catalogue, and the VEX overlay schema + example. Shipped so a
# downstream consumer building from a release tarball can run `make advisory`.
EXTRA_DIST += scripts/gen-advisory \
scripts/cwe-names.json \
scripts/advisory-vex-overlay.schema.json \
scripts/advisory-vex-overlay.example.json
# Advisory regression suite + CSAF 2.0 conformance gate, with the frozen CVE
# fixtures they run against. Shipped so a downstream consumer / CRA reviewer
# can re-run the advisory tests from a release tarball.
EXTRA_DIST += scripts/csaf_validate.mjs \
scripts/test_gen_advisory.py \
scripts/testdata/README.md \
scripts/testdata/CVE-2026-5501.json \
scripts/testdata/CVE-2026-5778.json \
scripts/testdata/CVE-2026-5999.json
+672
View File
@@ -0,0 +1,672 @@
#!/usr/bin/env python3
"""Unit + semantic tests for scripts/gen-advisory.
Run from the repo root:
python3 -m unittest scripts/test_gen_advisory.py
These tests are pure stdlib (no network, no pip deps) so they form the cheap
PR gate, mirroring scripts/test_gen_sbom.py. They cover three things the
JSON-schema validators in .github/workflows/advisory.yml do NOT:
1. the pure record->model logic (CVSS priority, CWE extraction, version
ranges, the FIPS product split, the reachability hedge);
2. CSAF *semantic* invariants that a bare JSON-schema pass accepts but the
CSAF mandatory tests reject (every referenced product_id is defined in
the product_tree, no product is simultaneously affected and not-affected,
flags only sit on not-affected products, scores only target affected
products, tracking.version matches the latest revision_history entry);
3. the two regressions already fixed once (CycloneDX uses `unaffected`
not `not_affected` in affects[].versions[].status; every CSAF reference
carries the required `summary`).
The full CSAF 2.0 schema + mandatory-test conformance and the CycloneDX 1.6
strict-schema pass run in CI against csaf-validator-lib / cyclonedx-bom; this
file deliberately avoids those heavyweight deps.
"""
import importlib.util
import json
import os
import pathlib
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from importlib.machinery import SourceFileLoader
HERE = pathlib.Path(__file__).resolve().parent
SCRIPT = HERE / 'gen-advisory'
TESTDATA = HERE / 'testdata'
EXAMPLE_OVERLAY = HERE / 'advisory-vex-overlay.example.json'
OVERLAY_SCHEMA = HERE / 'advisory-vex-overlay.schema.json'
# Pinned epoch -> 2023-11-14T22:13:20Z. Shared by the reproducibility test
# and the timestamp unit test so the expected string is single-sourced.
PINNED_EPOCH = '1700000000'
PINNED_EPOCH_ISO = '2023-11-14T22:13:20Z'
def _load_gen_advisory():
"""Load gen-advisory (no .py extension) as module 'ga', same trick as
test_gen_sbom.py uses for gen-sbom."""
if not SCRIPT.is_file():
raise FileNotFoundError(f"expected gen-advisory alongside this test at {SCRIPT}")
loader = SourceFileLoader('ga', str(SCRIPT))
spec = importlib.util.spec_from_loader('ga', loader)
module = importlib.util.module_from_spec(spec)
loader.exec_module(module)
return module
ga = _load_gen_advisory()
def _record(name):
with open(TESTDATA / name) as f:
return json.load(f)
def _adv(name):
return ga.parse_record(_record(name))
def _overlay():
with open(EXAMPLE_OVERLAY) as f:
return json.load(f)
def _collect_product_ids(node):
"""Every product_id declared anywhere in a CSAF product_tree branch."""
pids = set()
prod = node.get('product')
if isinstance(prod, dict) and 'product_id' in prod:
pids.add(prod['product_id'])
for child in node.get('branches', []):
pids |= _collect_product_ids(child)
return pids
def _tree_product_ids(doc):
pids = set()
for branch in doc['product_tree'].get('branches', []):
pids |= _collect_product_ids(branch)
return pids
# Valid CSAF 2.0 enum subsets we rely on (spec 6.1.* / schema enums).
CSAF_STATUS_BUCKETS = {
'first_affected', 'first_fixed', 'fixed', 'known_affected',
'known_not_affected', 'last_affected', 'recommended',
'under_investigation',
}
CSAF_FLAG_LABELS = {
'component_not_present', 'inline_mitigations_already_exist',
'vulnerable_code_cannot_be_controlled_by_adversary',
'vulnerable_code_not_in_execute_path', 'vulnerable_code_not_present',
}
CSAF_REMEDIATION_CATEGORIES = {
'mitigation', 'no_fix_planned', 'none_available', 'optional_patch',
'vendor_fix', 'workaround', 'fix_planned',
}
CDX_AFFECTS_STATUS = {'affected', 'unaffected', 'unknown'}
# --------------------------------------------------------------------------- #
# Pure helpers
# --------------------------------------------------------------------------- #
class TestDerivedUuid(unittest.TestCase):
def test_deterministic(self):
self.assertEqual(ga.derived_uuid('a', 'b'), ga.derived_uuid('a', 'b'))
def test_distinct_inputs_distinct_output(self):
self.assertNotEqual(ga.derived_uuid('a', 'b'), ga.derived_uuid('a', 'c'))
def test_no_aliasing_across_separator(self):
# NUL-separated join: ('a','bc') must not collide with ('ab','c').
self.assertNotEqual(ga.derived_uuid('a', 'bc'), ga.derived_uuid('ab', 'c'))
def test_is_uuid(self):
self.assertRegex(
ga.derived_uuid('x'),
r'^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$')
class TestBuildTimestamp(unittest.TestCase):
def setUp(self):
self._saved = os.environ.get('SOURCE_DATE_EPOCH')
def tearDown(self):
if self._saved is None:
os.environ.pop('SOURCE_DATE_EPOCH', None)
else:
os.environ['SOURCE_DATE_EPOCH'] = self._saved
def test_honors_source_date_epoch(self):
os.environ['SOURCE_DATE_EPOCH'] = PINNED_EPOCH
_, iso = ga.build_timestamp()
self.assertEqual(iso, PINNED_EPOCH_ISO)
def test_invalid_epoch_falls_back_to_now(self):
os.environ['SOURCE_DATE_EPOCH'] = 'not-a-number'
_, iso = ga.build_timestamp()
# Falls back to wallclock; just assert a well-formed Z timestamp.
self.assertRegex(iso, r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$')
class TestCpePurl(unittest.TestCase):
def test_cpe(self):
self.assertEqual(ga.cpe_for('wolfSSL', '5.9.1'),
'cpe:2.3:a:wolfssl:wolfssl:5.9.1:*:*:*:*:*:*:*')
def test_purl(self):
self.assertEqual(ga.purl_for('wolfSSL', '5.9.1'),
'pkg:github/wolfSSL/wolfssl@v5.9.1')
class TestBestCvss(unittest.TestCase):
def test_priority_v4_over_v3(self):
metrics = [{'cvssV3_1': {'x': 1}}, {'cvssV4_0': {'y': 2}}]
best = ga._best_cvss(metrics)
self.assertEqual(best['csaf_key'], 'cvss_v4')
self.assertEqual(best['cdx_method'], 'CVSSv4')
self.assertEqual(best['data'], {'y': 2})
def test_v31_over_v30_over_v2(self):
self.assertEqual(
ga._best_cvss([{'cvssV2_0': {}}, {'cvssV3_0': {}}])['csaf_key'],
'cvss_v3')
self.assertEqual(
ga._best_cvss([{'cvssV2_0': {}}])['csaf_key'], 'cvss_v2')
def test_none_when_absent(self):
self.assertIsNone(ga._best_cvss([]))
self.assertIsNone(ga._best_cvss([{'other': {}}]))
class TestParseRecord(unittest.TestCase):
def test_core_fields(self):
adv = _adv('CVE-2026-5501.json')
self.assertEqual(adv['cve'], 'CVE-2026-5501')
self.assertTrue(adv['title'].startswith('Improper Certificate'))
self.assertIn('wolfSSL_X509_verify_cert', adv['description'])
self.assertEqual(adv['date_published'], '2026-04-10T03:07:39.604Z')
self.assertEqual(adv['date_updated'], '2026-04-22T13:59:28.514Z')
def test_cwe_id_and_canonical_name(self):
adv = _adv('CVE-2026-5501.json')
self.assertEqual(adv['cwe']['id'], 'CWE-295')
# Resolved from the official catalogue (exact MITRE casing), NOT the
# record's lowercase free text -- required by CSAF test 6.1.11.
self.assertEqual(adv['cwe']['name'], 'Improper Certificate Validation')
def test_cvss_is_v4_and_no_csaf20_compatible_score(self):
adv = _adv('CVE-2026-5501.json')
self.assertEqual(adv['cvss']['csaf_key'], 'cvss_v4')
self.assertEqual(adv['cvss']['data']['baseSeverity'], 'CRITICAL')
self.assertEqual(adv['cvss']['data']['baseScore'], 9.3)
# The record carries only CVSS v4, which CSAF 2.0 scores[] cannot hold.
self.assertIsNone(adv['cvss_csaf'])
def test_affected_and_credits(self):
adv = _adv('CVE-2026-5501.json')
self.assertEqual(len(adv['affected']), 1)
a = adv['affected'][0]
self.assertEqual(a['product'], 'wolfSSL')
self.assertEqual(a['default_status'], 'unaffected')
self.assertEqual(a['versions'][0]['lessThanOrEqual'], '5.9.0')
self.assertEqual(adv['references'],
['https://github.com/wolfSSL/wolfssl/pull/10102'])
self.assertEqual(len(adv['credits']), 1)
def test_missing_cveid_exits(self):
with self.assertRaises(SystemExit):
ga.parse_record({'containers': {'cna': {}}, 'cveMetadata': {}})
def test_missing_cna_exits(self):
with self.assertRaises(SystemExit):
ga.parse_record({'cveMetadata': {'cveId': 'CVE-1'}})
class TestRangeLabelAndVers(unittest.TestCase):
def test_less_than_or_equal_from_zero(self):
v = {'version': '0', 'lessThanOrEqual': '5.9.0'}
self.assertEqual(ga._range_label(v), '<= 5.9.0')
self.assertEqual(ga._vers_range(v), 'vers:generic/<=5.9.0')
def test_less_than_with_base(self):
v = {'version': '5.0.0', 'lessThan': '5.9.0'}
self.assertEqual(ga._range_label(v), '5.0.0 <= x < 5.9.0')
self.assertEqual(ga._vers_range(v), 'vers:generic/>=5.0.0|<5.9.0')
def test_single_version(self):
v = {'version': '5.9.0'}
self.assertEqual(ga._range_label(v), '5.9.0')
self.assertEqual(ga._vers_range(v), 'vers:generic/5.9.0')
class TestProductModel(unittest.TestCase):
def test_mainline_only(self):
adv = _adv('CVE-2026-5501.json')
prods = ga.product_model(adv, {'state': 'exploitable',
'fixed_versions': ['5.9.1']})
self.assertEqual(len(prods), 1)
p = prods[0]
self.assertEqual(p['product_name'], 'wolfSSL')
self.assertEqual(p['bucket'], 'known_affected')
self.assertEqual(len(p['affected_ranges']), 1)
self.assertEqual(p['fixed'][0]['version'], '5.9.1')
self.assertEqual(p['remediation_category'], 'vendor_fix')
def test_not_affected_state_sets_bucket_and_justification(self):
adv = _adv('CVE-2026-5501.json')
prods = ga.product_model(
adv, {'state': 'not_affected', 'justification': 'code_not_present'})
self.assertEqual(prods[0]['bucket'], 'known_not_affected')
self.assertEqual(prods[0]['justification'], 'code_not_present')
def test_fips_modelled_as_second_product(self):
adv = _adv('CVE-2026-5501.json')
ov = _overlay()['CVE-2026-5501']
prods = ga.product_model(adv, ov)
self.assertEqual(len(prods), 2)
fips = [p for p in prods if p['cdx_key'] == 'wolfcrypt-fips'][0]
self.assertEqual(fips['bucket'], 'known_not_affected')
self.assertEqual(fips['justification'], 'code_not_present')
self.assertIn('CMVP Certificate #4718', fips['model_numbers'])
self.assertEqual(fips['module_version'], '5.2.1')
# not-affected FIPS with no fix => no_fix_planned, not none_available.
self.assertEqual(fips['remediation_category'], 'no_fix_planned')
class TestHedgeNote(unittest.TestCase):
def test_renders_defines_and_default_off(self):
note = ga._hedge_note({'requires_defines': ['WOLFSSL_SNIFFER'],
'default_status': 'off'})
self.assertIn('WOLFSSL_SNIFFER', note)
self.assertIn('disabled by default', note)
def test_none_when_empty(self):
self.assertIsNone(ga._hedge_note({}))
# --------------------------------------------------------------------------- #
# CSAF emitter: structure + semantic invariants
# --------------------------------------------------------------------------- #
class TestGenerateCsaf(unittest.TestCase):
def setUp(self):
self.ov = _overlay()
self.single = ga.generate_csaf(
[_adv('CVE-2026-5501.json')], self.ov, 'CVE-2026-5501',
PINNED_EPOCH_ISO)
self.bundle = ga.generate_csaf(
[_adv('CVE-2026-5501.json'), _adv('CVE-2026-5778.json')],
self.ov, 'wolfSSL-SA-5.9.1', PINNED_EPOCH_ISO)
def test_required_document_skeleton(self):
d = self.single['document']
self.assertEqual(d['csaf_version'], '2.0')
self.assertEqual(d['category'], 'csaf_security_advisory')
self.assertEqual(d['publisher']['category'], 'vendor')
self.assertEqual(d['tracking']['id'], 'CVE-2026-5501')
self.assertEqual(d['tracking']['status'], 'final')
self.assertIn('initial_release_date', d['tracking'])
self.assertIn('current_release_date', d['tracking'])
self.assertTrue(d['distribution']['tlp']['label'])
self.assertTrue(d['notes'])
def test_tracking_version_matches_latest_revision(self):
# CSAF 6.1.x: for a non-draft doc the latest revision_history number
# must equal tracking.version.
tr = self.single['document']['tracking']
latest = tr['revision_history'][-1]['number']
self.assertEqual(tr['version'], latest)
def test_document_references_have_summary(self):
# Regression: CSAF rejects references without `summary`.
for ref in self.single['document'].get('references', []):
self.assertIn('summary', ref)
self.assertTrue(ref['summary'])
def test_all_product_ids_defined_in_tree(self):
for doc in (self.single, self.bundle):
defined = _tree_product_ids(doc)
self.assertTrue(defined)
for v in doc['vulnerabilities']:
for bucket, pids in v.get('product_status', {}).items():
self.assertIn(bucket, CSAF_STATUS_BUCKETS)
self.assertTrue(set(pids) <= defined,
f'undefined pid in {bucket}')
for s in v.get('scores', []):
self.assertTrue(set(s['products']) <= defined)
for f in v.get('flags', []):
self.assertTrue(set(f['product_ids']) <= defined)
for r in v.get('remediations', []):
self.assertTrue(set(r['product_ids']) <= defined)
def test_no_product_both_affected_and_not_affected(self):
for v in self.bundle['vulnerabilities']:
ps = v.get('product_status', {})
affected = set(ps.get('known_affected', []))
not_affected = set(ps.get('known_not_affected', []))
self.assertEqual(affected & not_affected, set())
def test_vuln_references_have_summary(self):
for v in self.bundle['vulnerabilities']:
for ref in v.get('references', []):
self.assertIn('summary', ref)
def test_flags_only_on_not_affected_products(self):
for v in self.bundle['vulnerabilities']:
ps = v.get('product_status', {})
not_affected = set(ps.get('known_not_affected', []))
for f in v.get('flags', []):
self.assertIn(f['label'], CSAF_FLAG_LABELS)
self.assertTrue(set(f['product_ids']) <= not_affected)
def test_scores_only_target_affected(self):
for v in self.bundle['vulnerabilities']:
ps = v.get('product_status', {})
scoreable = set(ps.get('known_affected', [])) \
| set(ps.get('under_investigation', []))
for s in v.get('scores', []):
self.assertTrue(set(s['products']) <= scoreable)
def test_no_cvss_v4_in_csaf_scores(self):
# Regression: CSAF 2.0 scores[] has no cvss_v4 property; a v4 block
# there fails the strict schema. These records are v4-only, so no
# scores[] should be emitted at all.
for doc in (self.single, self.bundle):
for v in doc['vulnerabilities']:
for s in v.get('scores', []):
self.assertNotIn('cvss_v4', s)
def test_v4_only_record_emits_cvss_note(self):
# The v4 rating must not be silently dropped from CSAF: it is preserved
# as a note pointing at the CycloneDX VEX for the machine-readable form.
v = self.single['vulnerabilities'][0]
titles = [n.get('title') for n in v['notes']]
self.assertIn('CVSS v4.0', titles)
note = [n for n in v['notes'] if n.get('title') == 'CVSS v4.0'][0]
self.assertIn('9.3', note['text'])
def test_cwe_uses_canonical_catalogue_name(self):
v = [x for x in self.bundle['vulnerabilities']
if x['cve'] == 'CVE-2026-5778'][0]
self.assertEqual(v['cwe']['id'], 'CWE-191')
self.assertEqual(v['cwe']['name'],
'Integer Underflow (Wrap or Wraparound)')
def test_remediation_categories_valid(self):
for v in self.bundle['vulnerabilities']:
for r in v.get('remediations', []):
self.assertIn(r['category'], CSAF_REMEDIATION_CATEGORIES)
def test_fips_is_its_own_product_branch(self):
names = set()
def walk(node):
if node.get('category') == 'product_name':
names.add(node['name'])
for c in node.get('branches', []):
walk(c)
for b in self.single['product_tree']['branches']:
walk(b)
self.assertIn('wolfSSL', names)
self.assertTrue(any('FIPS' in n for n in names),
f'expected a FIPS product branch, got {names}')
def test_bundle_has_two_vulns_and_aggregate_severity(self):
self.assertEqual(len(self.bundle['vulnerabilities']), 2)
cves = {v['cve'] for v in self.bundle['vulnerabilities']}
self.assertEqual(cves, {'CVE-2026-5501', 'CVE-2026-5778'})
# CRITICAL (5501) outranks HIGH (5778).
self.assertEqual(self.bundle['document']['aggregate_severity']['text'],
'CRITICAL')
def test_hedge_note_present_for_sniffer_cve(self):
v = [x for x in self.bundle['vulnerabilities']
if x['cve'] == 'CVE-2026-5778'][0]
texts = ' '.join(n['text'] for n in v['notes'])
self.assertIn('WOLFSSL_SNIFFER', texts)
class TestCsafV3Scores(unittest.TestCase):
"""The v4-only fixtures never populate CSAF scores[]; this exercises the
positive path with a CVSS v3.1 record (CSAF 2.0 can carry v3)."""
def setUp(self):
self.ov = _overlay()
self.adv = _adv('CVE-2026-5999.json')
self.doc = ga.generate_csaf([self.adv], self.ov, 'CVE-2026-5999',
PINNED_EPOCH_ISO)
def test_parse_selects_v3_for_csaf(self):
self.assertEqual(self.adv['cvss']['csaf_key'], 'cvss_v3')
self.assertIsNotNone(self.adv['cvss_csaf'])
self.assertEqual(self.adv['cvss_csaf']['csaf_key'], 'cvss_v3')
self.assertEqual(self.adv['cvss_csaf']['data']['baseScore'], 7.5)
def test_csaf_emits_cvss_v3_score(self):
v = self.doc['vulnerabilities'][0]
self.assertEqual(len(v['scores']), 1)
score = v['scores'][0]
self.assertIn('cvss_v3', score)
self.assertNotIn('cvss_v4', score)
self.assertTrue(score['products'])
# v3 path -> no CVSS v4 fallback note.
self.assertNotIn('CVSS v4.0', [n.get('title') for n in v['notes']])
def test_aggregate_severity_from_v3(self):
self.assertEqual(self.doc['document']['aggregate_severity']['text'],
'HIGH')
# --------------------------------------------------------------------------- #
# CycloneDX VEX emitter
# --------------------------------------------------------------------------- #
class TestGenerateCdxVex(unittest.TestCase):
def setUp(self):
self.ov = _overlay()
self.bom = ga.generate_cdx_vex(
[_adv('CVE-2026-5501.json'), _adv('CVE-2026-5778.json')],
self.ov, 'wolfSSL-SA-5.9.1', PINNED_EPOCH_ISO)
def test_bom_skeleton(self):
self.assertEqual(self.bom['bomFormat'], 'CycloneDX')
self.assertEqual(self.bom['specVersion'], '1.6')
self.assertRegex(self.bom['serialNumber'], r'^urn:uuid:[0-9a-f-]{36}$')
self.assertEqual(self.bom['metadata']['component']['name'], 'wolfssl')
def test_fips_component_present(self):
names = {c['name'] for c in self.bom['components']}
self.assertTrue(any('FIPS' in n for n in names), names)
def test_affects_status_uses_unaffected_not_not_affected(self):
# Regression sentinel: CycloneDX affects[].versions[].status only
# accepts affected/unaffected/unknown; not_affected belongs to
# analysis.state alone.
for v in self.bom['vulnerabilities']:
for aff in v['affects']:
for ver in aff.get('versions', []):
self.assertIn(ver['status'], CDX_AFFECTS_STATUS)
def test_not_affected_fips_range_is_unaffected(self):
v = [x for x in self.bom['vulnerabilities']
if x['id'] == 'CVE-2026-5501'][0]
# the FIPS component is not_affected -> its range status is unaffected.
fips_refs = {c['bom-ref'] for c in self.bom['components']}
fips_affects = [a for a in v['affects'] if a['ref'] in fips_refs]
self.assertTrue(fips_affects)
for a in fips_affects:
for ver in a['versions']:
self.assertEqual(ver['status'], 'unaffected')
def test_analysis_state_and_cwe_and_rating(self):
v = [x for x in self.bom['vulnerabilities']
if x['id'] == 'CVE-2026-5501'][0]
self.assertEqual(v['analysis']['state'], 'exploitable')
self.assertEqual(v['cwes'], [295])
self.assertEqual(v['ratings'][0]['method'], 'CVSSv4')
self.assertEqual(v['ratings'][0]['severity'], 'critical')
# --------------------------------------------------------------------------- #
# Overlay matches its own schema vocabulary (lightweight, no jsonschema).
# The authoritative jsonschema pass runs in CI; this guards the committed
# example overlay against drift without adding a pip dep to the unit gate.
# --------------------------------------------------------------------------- #
class TestExampleOverlay(unittest.TestCase):
def setUp(self):
with open(OVERLAY_SCHEMA) as f:
self.schema = json.load(f)
self.overlay = _overlay()
def _enum(self, name):
return set(self.schema['$defs'][name]['enum'])
def test_states_and_justifications_in_vocab(self):
states = self._enum('analysisState')
justifications = self._enum('justification')
for cve, entry in self.overlay.items():
if cve.startswith('_'):
continue
if 'state' in entry:
self.assertIn(entry['state'], states)
if 'justification' in entry:
self.assertIn(entry['justification'], justifications)
fips = entry.get('fips', {})
if 'status' in fips:
self.assertIn(fips['status'], states)
if 'justification' in fips:
self.assertIn(fips['justification'], justifications)
def test_not_affected_requires_justification(self):
for cve, entry in self.overlay.items():
if cve.startswith('_'):
continue
if entry.get('state') == 'not_affected':
self.assertIn('justification', entry)
if entry.get('fips', {}).get('status') == 'not_affected':
self.assertIn('justification', entry['fips'])
# --------------------------------------------------------------------------- #
# End-to-end via the CLI: reproducibility + fail-loud behaviour.
# --------------------------------------------------------------------------- #
class TestCliBehaviour(unittest.TestCase):
def _run(self, args, env=None):
e = dict(os.environ)
if env:
e.update(env)
return subprocess.run([sys.executable, str(SCRIPT)] + args,
capture_output=True, text=True, env=e)
def test_reproducible_under_source_date_epoch(self):
with tempfile.TemporaryDirectory() as d:
outs = []
for i in (1, 2):
csaf = os.path.join(d, f'a{i}.csaf.json')
cdx = os.path.join(d, f'a{i}.cdx.json')
r = self._run([
'--cve-record', str(TESTDATA / 'CVE-2026-5501.json'),
'--cve-record', str(TESTDATA / 'CVE-2026-5778.json'),
'--vex-overlay', str(EXAMPLE_OVERLAY),
'--advisory-id', 'wolfSSL-SA-5.9.1',
'--csaf-out', csaf, '--cdx-vex-out', cdx],
env={'SOURCE_DATE_EPOCH': PINNED_EPOCH})
self.assertEqual(r.returncode, 0, r.stderr)
with open(csaf, 'rb') as f:
csaf_b = f.read()
with open(cdx, 'rb') as f:
cdx_b = f.read()
outs.append((csaf_b, cdx_b))
self.assertEqual(outs[0][0], outs[1][0], 'CSAF not reproducible')
self.assertEqual(outs[0][1], outs[1][1], 'CDX not reproducible')
def test_single_record_defaults_advisory_id_to_cve(self):
with tempfile.TemporaryDirectory() as d:
csaf = os.path.join(d, 'one.csaf.json')
r = self._run([
'--cve-record', str(TESTDATA / 'CVE-2026-5501.json'),
'--vex-overlay', str(EXAMPLE_OVERLAY),
'--csaf-out', csaf])
self.assertEqual(r.returncode, 0, r.stderr)
with open(csaf) as f:
doc = json.load(f)
self.assertEqual(doc['document']['tracking']['id'],
'CVE-2026-5501')
def test_bundling_without_advisory_id_fails(self):
with tempfile.TemporaryDirectory() as d:
csaf = os.path.join(d, 'x.csaf.json')
r = self._run([
'--cve-record', str(TESTDATA / 'CVE-2026-5501.json'),
'--cve-record', str(TESTDATA / 'CVE-2026-5778.json'),
'--csaf-out', csaf])
self.assertNotEqual(r.returncode, 0)
self.assertFalse(os.path.exists(csaf),
'no output should be written on error')
def test_empty_records_dir_fails(self):
with tempfile.TemporaryDirectory() as d:
recs = os.path.join(d, 'records')
os.makedirs(recs)
r = self._run(['--records-dir', recs, '--out-dir', d])
self.assertNotEqual(r.returncode, 0)
self.assertIn('no CVE records found', r.stderr)
def test_batch_mode_writes_per_cve_documents(self):
with tempfile.TemporaryDirectory() as d:
recs = os.path.join(d, 'records')
os.makedirs(recs)
shutil.copy(str(TESTDATA / 'CVE-2026-5501.json'),
os.path.join(recs, 'CVE-2026-5501.json'))
shutil.copy(str(TESTDATA / 'CVE-2026-5999.json'),
os.path.join(recs, 'CVE-2026-5999.json'))
out = os.path.join(d, 'out')
r = self._run(['--records-dir', recs, '--out-dir', out,
'--vex-overlay', str(EXAMPLE_OVERLAY)])
self.assertEqual(r.returncode, 0, r.stderr)
for cve in ('CVE-2026-5501', 'CVE-2026-5999'):
csaf = os.path.join(out, f'{cve}.csaf.json')
cdx = os.path.join(out, f'{cve}.cdx.json')
self.assertTrue(os.path.exists(csaf), csaf)
self.assertTrue(os.path.exists(cdx), cdx)
with open(csaf) as f:
doc = json.load(f)
self.assertEqual(doc['document']['tracking']['id'], cve)
def test_default_records_dir_is_canonical_tree(self):
# No --cve-record/--cve-id and no --records-dir: must fall back to the
# canonical advisories/records/ tree (the same inputs `make advisory`
# uses). Output is redirected to a temp dir so the repo is untouched.
with tempfile.TemporaryDirectory() as d:
r = self._run(['--out-dir', d])
self.assertEqual(r.returncode, 0, r.stderr)
produced = sorted(f for f in os.listdir(d)
if f.endswith('.csaf.json'))
self.assertIn('CVE-2026-5501.csaf.json', produced)
self.assertIn('CVE-2026-5778.csaf.json', produced)
def test_malformed_record_fails_without_writing(self):
with tempfile.TemporaryDirectory() as d:
bad = os.path.join(d, 'bad.json')
with open(bad, 'w') as f:
f.write('{ this is not json')
csaf = os.path.join(d, 'out.csaf.json')
r = self._run(['--cve-record', bad, '--csaf-out', csaf])
self.assertNotEqual(r.returncode, 0)
self.assertFalse(os.path.exists(csaf))
if __name__ == '__main__':
unittest.main()
File diff suppressed because it is too large Load Diff
+122
View File
@@ -0,0 +1,122 @@
{
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"cveMetadata": {
"cveId": "CVE-2026-5501",
"assignerOrgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"state": "PUBLISHED",
"assignerShortName": "wolfSSL",
"dateReserved": "2026-04-03T15:46:09.302Z",
"datePublished": "2026-04-10T03:07:39.604Z",
"dateUpdated": "2026-04-22T13:59:28.514Z"
},
"containers": {
"cna": {
"providerMetadata": {
"orgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"shortName": "wolfSSL",
"dateUpdated": "2026-04-10T03:07:39.604Z"
},
"title": "Improper Certificate Signature Verification in X.509 Chain Validation Allows Forged Leaf Certificates",
"problemTypes": [
{
"descriptions": [
{
"lang": "en",
"cweId": "CWE-295",
"description": "CWE-295 Improper certificate validation",
"type": "CWE"
}
]
}
],
"affected": [
{
"vendor": "wolfSSL",
"product": "wolfSSL",
"modules": [
"wolfSSL_X509_verify_cert"
],
"programFiles": [
"src/x509_str.c"
],
"versions": [
{
"status": "affected",
"version": "0",
"lessThanOrEqual": "5.9.0",
"versionType": "semver"
}
],
"defaultStatus": "unaffected"
}
],
"descriptions": [
{
"lang": "en",
"value": "wolfSSL_X509_verify_cert in the OpenSSL compatibility layer accepts a certificate chain in which the leaf's signature is not checked, if the attacker supplies an untrusted intermediate with Basic Constraints `CA:FALSE` that is legitimately signed by a trusted root. An attacker who obtains any leaf certificate from a trusted CA (e.g. a free DV cert from Let's Encrypt) can forge a certificate for any subject name with any public key and arbitrary signature bytes, and the function returns `WOLFSSL_SUCCESS` / `X509_V_OK`. The native wolfSSL TLS handshake path (`ProcessPeerCerts`) is not susceptible and the issue is limited to applications using the OpenSSL compatibility API directly, which would include integrations of wolfSSL into nginx and haproxy.",
"supportingMedia": [
{
"type": "text/html",
"base64": false,
"value": "wolfSSL_X509_verify_cert in the OpenSSL compatibility layer accepts a certificate chain in which the leaf's signature is not checked, if the attacker supplies an untrusted intermediate with Basic Constraints `CA:FALSE` that is legitimately signed by a trusted root. An attacker who obtains any leaf certificate from a trusted CA (e.g. a free DV cert from Let's Encrypt) can forge a certificate for any subject name with any public key and arbitrary signature bytes, and the function returns `WOLFSSL_SUCCESS` / `X509_V_OK`. The native wolfSSL TLS handshake path (`ProcessPeerCerts`) is not susceptible and the issue is limited to applications using the OpenSSL compatibility API directly, which would include integrations of wolfSSL into nginx and haproxy."
}
]
}
],
"references": [
{
"url": "https://github.com/wolfSSL/wolfssl/pull/10102"
}
],
"metrics": [
{
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
],
"cvssV4_0": {
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"attackRequirements": "NONE",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"vulnConfidentialityImpact": "HIGH",
"subConfidentialityImpact": "NONE",
"vulnIntegrityImpact": "HIGH",
"subIntegrityImpact": "NONE",
"vulnAvailabilityImpact": "NONE",
"subAvailabilityImpact": "NONE",
"exploitMaturity": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"valueDensity": "NOT_DEFINED",
"vulnerabilityResponseEffort": "NOT_DEFINED",
"providerUrgency": "NOT_DEFINED",
"version": "4.0",
"baseSeverity": "CRITICAL",
"baseScore": 9.3,
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:N/SC:N/SI:N/SA:N"
}
}
],
"credits": [
{
"lang": "en",
"value": "Calif.io in collaboration with Claude and Anthropic Research",
"type": "finder"
}
],
"source": {
"discovery": "EXTERNAL"
},
"x_generator": {
"engine": "Vulnogram 1.0.1"
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
{
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"cveMetadata": {
"cveId": "CVE-2026-5778",
"assignerOrgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"state": "PUBLISHED",
"assignerShortName": "wolfSSL",
"dateReserved": "2026-04-08T08:25:15.400Z",
"datePublished": "2026-04-09T21:45:09.053Z",
"dateUpdated": "2026-04-10T13:53:29.181Z"
},
"containers": {
"cna": {
"providerMetadata": {
"orgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"shortName": "wolfSSL",
"dateUpdated": "2026-04-09T21:45:09.053Z"
},
"title": "Integer underflow leads to out-of-bounds access in sniffer ChaCha decrypt path.",
"problemTypes": [
{
"descriptions": [
{
"lang": "en",
"cweId": "CWE-191",
"description": "CWE-191 Integer underflow (wrap or wraparound)",
"type": "CWE"
}
]
}
],
"affected": [
{
"vendor": "wolfSSL",
"product": "wolfSSL",
"modules": [
"Packet sniffer"
],
"programFiles": [
"src/sniffer.c"
],
"versions": [
{
"status": "affected",
"version": "0",
"lessThanOrEqual": "5.9.0",
"versionType": "semver"
}
],
"defaultStatus": "unaffected"
}
],
"descriptions": [
{
"lang": "en",
"value": "Integer underflow in wolfSSL packet sniffer <= 5.9.0 allows an attacker to cause a program crash in the AEAD decryption path by injecting a TLS record shorter than the explicit IV plus authentication tag into traffic inspected by ssl_DecodePacket. The underflow wraps a 16-bit length to a large value that is passed to AEAD decryption routines, causing a large out-of-bounds read and crash. An unauthenticated attacker can trigger this remotely via malformed TLS Application Data records.",
"supportingMedia": [
{
"type": "text/html",
"base64": false,
"value": "Integer underflow in wolfSSL packet sniffer &lt;= 5.9.0 allows an attacker to cause a program crash in the AEAD decryption path by injecting a TLS record shorter than the explicit IV plus authentication tag into traffic inspected by ssl_DecodePacket. The underflow wraps a 16-bit length to a large value that is passed to AEAD decryption routines, causing a large out-of-bounds read and crash. An unauthenticated attacker can trigger this remotely via malformed TLS Application Data records."
}
]
}
],
"references": [
{
"url": "https://github.com/wolfSSL/wolfssl/pull/10125"
}
],
"metrics": [
{
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
],
"cvssV4_0": {
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"attackRequirements": "PRESENT",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"vulnConfidentialityImpact": "NONE",
"subConfidentialityImpact": "NONE",
"vulnIntegrityImpact": "NONE",
"subIntegrityImpact": "NONE",
"vulnAvailabilityImpact": "HIGH",
"subAvailabilityImpact": "NONE",
"exploitMaturity": "NOT_DEFINED",
"Safety": "NOT_DEFINED",
"Automatable": "NOT_DEFINED",
"Recovery": "NOT_DEFINED",
"valueDensity": "NOT_DEFINED",
"vulnerabilityResponseEffort": "NOT_DEFINED",
"providerUrgency": "NOT_DEFINED",
"version": "4.0",
"baseSeverity": "HIGH",
"baseScore": 8.2,
"vectorString": "CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N"
}
}
],
"credits": [
{
"lang": "en",
"value": "Zou Dikai",
"type": "finder"
}
],
"source": {
"discovery": "EXTERNAL"
},
"x_generator": {
"engine": "Vulnogram 1.0.1"
}
}
}
}
+99
View File
@@ -0,0 +1,99 @@
{
"dataType": "CVE_RECORD",
"dataVersion": "5.2",
"cveMetadata": {
"cveId": "CVE-2026-5999",
"assignerOrgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"state": "PUBLISHED",
"assignerShortName": "wolfSSL",
"dateReserved": "2026-04-15T09:00:00.000Z",
"datePublished": "2026-04-18T12:00:00.000Z",
"dateUpdated": "2026-04-18T12:00:00.000Z"
},
"containers": {
"cna": {
"providerMetadata": {
"orgId": "50d2cd11-d01a-48ed-9441-5bfce9d63b27",
"shortName": "wolfSSL",
"dateUpdated": "2026-04-18T12:00:00.000Z"
},
"title": "Out-of-bounds read parsing a malformed DTLS handshake message.",
"problemTypes": [
{
"descriptions": [
{
"lang": "en",
"cweId": "CWE-125",
"description": "CWE-125 Out-of-bounds read",
"type": "CWE"
}
]
}
],
"affected": [
{
"vendor": "wolfSSL",
"product": "wolfSSL",
"programFiles": [
"src/dtls.c"
],
"versions": [
{
"status": "affected",
"version": "0",
"lessThanOrEqual": "5.9.0",
"versionType": "semver"
}
],
"defaultStatus": "unaffected"
}
],
"descriptions": [
{
"lang": "en",
"value": "A synthetic test fixture (not a real CVE). An out-of-bounds read in wolfSSL DTLS handshake parsing <= 5.9.0 allows a remote unauthenticated attacker to read past the end of a record buffer by sending a malformed handshake message, potentially crashing the server. This record exists to exercise the CVSS v3.1 scores[] path of gen-advisory."
}
],
"references": [
{
"url": "https://github.com/wolfSSL/wolfssl/pull/99999"
}
],
"metrics": [
{
"format": "CVSS",
"scenarios": [
{
"lang": "en",
"value": "GENERAL"
}
],
"cvssV3_1": {
"version": "3.1",
"attackVector": "NETWORK",
"attackComplexity": "LOW",
"privilegesRequired": "NONE",
"userInteraction": "NONE",
"scope": "UNCHANGED",
"confidentialityImpact": "HIGH",
"integrityImpact": "NONE",
"availabilityImpact": "NONE",
"baseScore": 7.5,
"baseSeverity": "HIGH",
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
}
}
],
"credits": [
{
"lang": "en",
"value": "wolfSSL internal testing",
"type": "finder"
}
],
"source": {
"discovery": "INTERNAL"
}
}
}
}
+11
View File
@@ -0,0 +1,11 @@
# gen-advisory test fixtures
CVE Program records (CVE JSON 5.x) used by `scripts/test_gen_advisory.py` and
the `.github/workflows/advisory.yml` jobs. Committed so the tests are hermetic
(no network fetch from cve.org at test time).
| File | Provenance |
|------|------------|
| `CVE-2026-5501.json` | Real published wolfSSL CNA record (CVSS v4 only). |
| `CVE-2026-5778.json` | Real published wolfSSL CNA record (CVSS v4 only). |
| `CVE-2026-5999.json` | **Synthetic fixture, not a real CVE.** Carries a CVSS v3.1 block so the CSAF `scores[]` emission path (and the CVSS-consistency mandatory tests 6.1.8/6.1.9) is exercised; the v4-only records above never populate `scores[]` in CSAF 2.0. |