Skip to content

Commit 2c5d9bf

Browse files
committed
fix(sbom): strip + inside CPE and PURL constructors
Keep identifier_version() as the single strip, and apply it in cpe23_uri() and every DEP_META lambda. A caller that passes a raw OpenSSL BUILD_METADATA string then still emits a legal CPE 2.3 URI. version / versionInfo still record the full string. Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
1 parent 033239a commit 2c5d9bf

3 files changed

Lines changed: 60 additions & 19 deletions

File tree

share/gen-sbom

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ from datetime import datetime, timezone
2828
# 1.8 CPE and PURL identifiers drop `+` build metadata (OpenSSL
2929
# BUILD_METADATA, PEP 440 local versions). A raw `+` is not legal
3030
# in CPE 2.3 and does not match an upstream git tag. version /
31-
# versionInfo still records the full string.
31+
# versionInfo still records the full string. Constructors (cpe23_uri,
32+
# wolfssl_project_purl, DEP_META lambdas) strip; this is not a
33+
# full CPE sanitizer.
3234
# 1.7 wolfBoot CPE is registered in the NVD Official CPE Dictionary
3335
# (published 2026-08-10). PRODUCT_CPE status flips to registered so
3436
# the main package emits `cpe` and the pending properties stop.
@@ -140,12 +142,21 @@ def identifier_version(version):
140142
A raw `+` is not a legal CPE 2.3 version character, and the suffix
141143
is not an upstream git tag, so NVD / OSV cannot match it. Keep the
142144
full string in version / versionInfo; use this for identifiers.
145+
146+
This is not a full CPE 2.3 sanitizer: other illegal characters
147+
(`:`, `*`, whitespace) are not rewritten.
143148
"""
144149
if not version:
145150
return version
146151
return version.split('+', 1)[0]
147152

148153

154+
def cpe23_uri(vendor, product, version):
155+
"""CPE 2.3 application URI. Version uses identifier_version()."""
156+
ident = identifier_version(version)
157+
return f'cpe:2.3:a:{vendor}:{product}:{ident}:*:*:*:*:*:*:*'
158+
159+
149160
def wolfssl_project_purl(name, version):
150161
"""Canonical pkg:github PURL for a wolfSSL-stack project."""
151162
return github_purl(
@@ -183,10 +194,7 @@ PRODUCT_CPE = {
183194

184195

185196
def _product_cpe_string(meta, version):
186-
ident = identifier_version(version)
187-
return (
188-
f"cpe:2.3:a:{meta['vendor']}:{meta['product']}:{ident}:*:*:*:*:*:*:*"
189-
)
197+
return cpe23_uri(meta['vendor'], meta['product'], version)
190198

191199

192200
def product_cpe(name, version):
@@ -313,7 +321,7 @@ DEP_META = {
313321
'pkgconfig': 'wolfssl',
314322
'purl': lambda v: wolfssl_project_purl('wolfssl', v),
315323
# The CPE NVD registers for the wolfSSL library.
316-
'cpe': lambda v: f'cpe:2.3:a:wolfssl:wolfssl:{v}:*:*:*:*:*:*:*',
324+
'cpe': lambda v: cpe23_uri('wolfssl', 'wolfssl', v),
317325
},
318326
# wolfCrypt is a separate NVD product (cpe:2.3:a:wolfssl:wolfcrypt).
319327
# Embedders such as wolfBoot compile wolfcrypt sources into the image;
@@ -334,7 +342,7 @@ DEP_META = {
334342
# does not collide with the wolfssl component's own PURL. NVD
335343
# matching keys on the wolfcrypt CPE below.
336344
'purl': lambda v: wolfssl_project_purl('wolfssl', v) + '#wolfcrypt',
337-
'cpe': lambda v: f'cpe:2.3:a:wolfssl:wolfcrypt:{v}:*:*:*:*:*:*:*',
345+
'cpe': lambda v: cpe23_uri('wolfssl', 'wolfcrypt', v),
338346
},
339347
'libz': {
340348
'name': 'zlib',
@@ -345,8 +353,9 @@ DEP_META = {
345353
# pkg:github resolves in OSV / GHSA / Snyk / Trivy without the
346354
# vendor:product mapping a pkg:generic PURL would force. zlib tags
347355
# its releases `vX.Y.Z`, so the bare pkg-config version needs the `v`.
348-
'purl': lambda v: github_purl('madler', 'zlib', f'v{v}'),
349-
'cpe': lambda v: f'cpe:2.3:a:zlib:zlib:{v}:*:*:*:*:*:*:*',
356+
'purl': lambda v: github_purl(
357+
'madler', 'zlib', f'v{identifier_version(v)}'),
358+
'cpe': lambda v: cpe23_uri('zlib', 'zlib', v),
350359
},
351360
# openssl, declared as a dependency by the OpenSSL-compat products
352361
# (wolfProvider, wolfEngine) that link libcrypto/libssl alongside wolfSSL.
@@ -361,8 +370,9 @@ DEP_META = {
361370
'license': 'Apache-2.0',
362371
'download': 'https://github.com/openssl/openssl',
363372
'pkgconfig': 'openssl',
364-
'purl': lambda v: github_purl('openssl', 'openssl', f'openssl-{v}'),
365-
'cpe': lambda v: f'cpe:2.3:a:openssl:openssl:{v}:*:*:*:*:*:*:*',
373+
'purl': lambda v: github_purl(
374+
'openssl', 'openssl', f'openssl-{identifier_version(v)}'),
375+
'cpe': lambda v: cpe23_uri('openssl', 'openssl', v),
366376
},
367377
}
368378

@@ -1007,13 +1017,14 @@ def cdx_dep_component(name, pkg_version, key, dep_version_overrides=None):
10071017
'externalReferences': [{'type': 'vcs', 'url': meta['download']}],
10081018
}
10091019
if version:
1010-
ident = identifier_version(version)
1020+
# Pass the full version: constructors apply identifier_version().
1021+
# version / versionInfo keep the local string (BUILD_METADATA).
10111022
comp['version'] = version
1012-
comp['purl'] = meta['purl'](ident)
1023+
comp['purl'] = meta['purl'](version)
10131024
# Both identifiers are version-bearing, so neither can be emitted
10141025
# without a resolved version: a CPE with an empty version field
10151026
# matches every release of the dependency in an NVD scan.
1016-
comp['cpe'] = meta['cpe'](ident)
1027+
comp['cpe'] = meta['cpe'](version)
10171028
else:
10181029
print(f"WARNING: version unknown for {meta['name']}; "
10191030
"omitting version, purl and cpe", file=sys.stderr)
@@ -1037,17 +1048,16 @@ def spdx_dep_package(key, dep_version_overrides=None):
10371048
'copyrightText': 'NOASSERTION',
10381049
}
10391050
if version:
1040-
ident = identifier_version(version)
10411051
pkg['externalRefs'] = [
10421052
{
10431053
'referenceCategory': 'SECURITY',
10441054
'referenceType': 'cpe23Type',
1045-
'referenceLocator': meta['cpe'](ident),
1055+
'referenceLocator': meta['cpe'](version),
10461056
},
10471057
{
10481058
'referenceCategory': 'PACKAGE-MANAGER',
10491059
'referenceType': 'purl',
1050-
'referenceLocator': meta['purl'](ident),
1060+
'referenceLocator': meta['purl'](version),
10511061
},
10521062
]
10531063
return spdx_id, pkg

share/sbom.am

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
# build-time dependency on a separate wolfSSL checkout.
1111
#
1212
# ---------------------------------------------------------------------------
13-
# The including Makefile.am MUST set, before `include scripts/sbom.am`:
13+
# The including Makefile.am MUST set, before `include tools/sbom/sbom.am`:
1414
# SBOM_PKGNAME Product name recorded in the SBOM (e.g. wolfssh). Drives
1515
# the output filenames and gen-sbom --name.
1616
# SBOM_LICENSE_FILE Path to the product's LICENSING file
@@ -102,7 +102,7 @@ SBOM_LIB_GLOBS = \
102102

103103
# Automake requires CLEANFILES to be initialised with `=` before `+=`; the
104104
# including Makefile.am must declare `CLEANFILES =` (typically in its primaries
105-
# init block) before `include scripts/sbom.am`.
105+
# init block) before `include tools/sbom/sbom.am`.
106106
CLEANFILES += $(SBOM_CDX) $(SBOM_SPDX) $(SBOM_SPDX_TV)
107107

108108
.PHONY: sbom install-sbom uninstall-sbom

tests/test_gen_sbom.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,14 +887,45 @@ def test_identifier_version_strips_plus_build_metadata(self):
887887
self.assertEqual(gs.identifier_version('3.5.4'), '3.5.4')
888888
self.assertEqual(
889889
gs.identifier_version('3.5.4+wolfProvider-nonfips'), '3.5.4')
890+
self.assertEqual(
891+
gs.identifier_version('1.2.3+foo+bar'), '1.2.3')
890892
self.assertIsNone(gs.identifier_version(None))
891893
self.assertEqual(gs.identifier_version(''), '')
892894

895+
def test_cpe23_uri_strips_plus_build_metadata(self):
896+
self.assertEqual(
897+
gs.cpe23_uri('openssl', 'openssl', '3.5.4+wolfProvider-nonfips'),
898+
'cpe:2.3:a:openssl:openssl:3.5.4:*:*:*:*:*:*:*')
899+
self.assertEqual(
900+
gs.product_cpe('wolfssl', '5.8.2+dirty'),
901+
'cpe:2.3:a:wolfssl:wolfssl:5.8.2:*:*:*:*:*:*:*')
902+
903+
def test_dep_constructors_strip_plus_build_metadata(self):
904+
# Constructors, not only cdx_dep_component / spdx_dep_package, must
905+
# strip. A later caller that passes the raw openssl version string
906+
# into meta['cpe'] must still emit a legal CPE 2.3 URI.
907+
dirty = '1.2.3+local'
908+
for key, meta in gs.DEP_META.items():
909+
with self.subTest(dep=key):
910+
cpe = meta['cpe'](dirty)
911+
self.assertNotIn('+', cpe, cpe)
912+
self.assertIn(':1.2.3:', cpe)
913+
purl = meta['purl'](dirty)
914+
self.assertNotIn('+', purl, purl)
915+
self.assertIn('1.2.3', purl)
916+
893917
def test_openssl_dep_cpe_and_purl_drop_build_metadata(self):
894918
# wolfProvider patches OpenSSL BUILD_METADATA, so openssl version
895919
# prints 3.5.4+wolfProvider-nonfips. CPE 2.3 rejects a raw `+`, and
896920
# the git tag is openssl-3.5.4. versionInfo keeps the local string.
897921
dirty = '3.5.4+wolfProvider-nonfips'
922+
openssl = gs.DEP_META['openssl']
923+
self.assertEqual(
924+
openssl['cpe'](dirty),
925+
'cpe:2.3:a:openssl:openssl:3.5.4:*:*:*:*:*:*:*')
926+
self.assertEqual(
927+
openssl['purl'](dirty),
928+
'pkg:github/openssl/openssl@openssl-3.5.4')
898929
_, cdx = gs.cdx_dep_component(
899930
'wolfprovider', '1.2.1', 'openssl', {'openssl': dirty})
900931
self.assertEqual(cdx['version'], dirty)

0 commit comments

Comments
 (0)