mirror of
https://github.com/wolfSSL/wolfssl.git
synced 2026-08-10 17:51:20 +02:00
gen-sbom: detect GPLv3 abbreviation and support downstream products
Recognize the "GPLv3"/"GPLv3+" short form so downstream LICENSING files map to GPL-3.0-only instead of NOASSERTION. Add --dep-wolfssl, a wolfssl DEP_META entry, and --name-derived project URLs. Update tests. Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
This commit is contained in:
committed by
Mark Atwood
parent
4ec80d309a
commit
64b4e441d0
+73
-12
@@ -53,6 +53,21 @@ _NO_HASH_NOTE = (
|
||||
SBOM_UUID_NAMESPACE = uuid.uuid5(uuid.NAMESPACE_URL, 'https://wolfssl.com/sbom/')
|
||||
|
||||
|
||||
def project_urls(name):
|
||||
"""Canonical wolfSSL GitHub URLs for a project, derived from its package
|
||||
name. Keeping these name-derived (rather than hardcoded to wolfssl) lets
|
||||
the same generator emit correct VCS / issue-tracker / advisory / download
|
||||
URLs for every product in the wolfSSL stack (wolfssl, wolfssh, wolfmqtt,
|
||||
...). For name='wolfssl' the result is byte-identical to the historical
|
||||
hardcoded URLs, so existing wolfSSL SBOMs do not change."""
|
||||
base = f'https://github.com/wolfSSL/{name}'
|
||||
return {
|
||||
'vcs': base,
|
||||
'issues': f'{base}/issues',
|
||||
'advisories': f'{base}/security/advisories',
|
||||
}
|
||||
|
||||
|
||||
def derived_uuid(*parts):
|
||||
"""Deterministic UUID from joined parts under the wolfSSL SBOM namespace.
|
||||
Re-runs of `make sbom` against the same source produce identical UUIDs,
|
||||
@@ -88,6 +103,25 @@ def build_timestamp():
|
||||
# / Dependency-Track resolve CVEs against the right package). Algorithm
|
||||
# enablement is captured separately via build_props (HAVE_FALCON, ...).
|
||||
DEP_META = {
|
||||
# wolfssl itself, declared as a dependency by downstream wolfSSL-stack
|
||||
# products (wolfSSH, wolfMQTT, wolfTPM, ...) that link libwolfssl. Only
|
||||
# emitted when the caller passes --dep-wolfssl yes; wolfSSL's own
|
||||
# `make sbom` never enables it (a package is not its own dependency).
|
||||
# Recording it is what lets a CRA / vulnerability scanner associate
|
||||
# wolfSSL advisories with a product that embeds wolfSSL.
|
||||
'wolfssl': {
|
||||
'name': 'wolfssl',
|
||||
'supplier': 'wolfSSL Inc.',
|
||||
# wolfSSL is distributed under GPLv3 (LICENSING: "version 3 (GPLv3)",
|
||||
# no "or later"), with a commercial option. This matches what
|
||||
# detect_license() infers for wolfSSL's own main-package SBOM, so a
|
||||
# downstream product's wolfssl dependency entry and wolfSSL's own
|
||||
# self-SBOM agree on the licence.
|
||||
'license': 'GPL-3.0-only',
|
||||
'download': 'https://github.com/wolfSSL/wolfssl',
|
||||
'pkgconfig': 'wolfssl',
|
||||
'purl': lambda v: f'pkg:github/wolfSSL/wolfssl@v{v}',
|
||||
},
|
||||
# liboqs is the only PQ external dependency wolfSSL still links against
|
||||
# after upstream PR #10293 collapsed the rest of the PQ surface into
|
||||
# native wolfCrypt. Today, --enable-falcon strictly implies --with-liboqs
|
||||
@@ -222,12 +256,25 @@ def detect_license(license_file):
|
||||
r'gnu general public license\s+version\s+(\d+)',
|
||||
text, re.IGNORECASE
|
||||
)
|
||||
or_later_plus = False
|
||||
if not m:
|
||||
# Abbreviated form: some wolfSSL-stack LICENSING files (e.g. wolfSSH)
|
||||
# say "GPLv3" rather than the canonical "GNU General Public License
|
||||
# version 3", so the long-form regex above misses and detection would
|
||||
# fall back to NOASSERTION. A trailing "+" (GPLv3+) denotes the
|
||||
# or-later variant; otherwise fall through to the shared "or later"
|
||||
# prose check below.
|
||||
m = re.search(r'\bGPLv(\d+)(\+)?', text, re.IGNORECASE)
|
||||
if m and m.group(2) == '+':
|
||||
or_later_plus = True
|
||||
if not m:
|
||||
print(f"WARNING: no GPL version found in {license_file}",
|
||||
file=sys.stderr)
|
||||
return None
|
||||
|
||||
version = m.group(1)
|
||||
if or_later_plus:
|
||||
return f'GPL-{version}.0-or-later'
|
||||
excerpt = text[m.end():m.end() + 100]
|
||||
# Match upgrade-permission wording in the 100-byte excerpt that
|
||||
# follows the version mention. Three FSF-derived shapes:
|
||||
@@ -742,6 +789,7 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
dep_version_overrides=None, hash_kind='library-binary',
|
||||
hash_source='lib', srcs_basenames=None, file_entries=None):
|
||||
bom_ref = derived_uuid(name, version, 'package')
|
||||
urls = project_urls(name)
|
||||
|
||||
dep_bom_refs = []
|
||||
components = []
|
||||
@@ -793,13 +841,13 @@ def generate_cdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
'hashes': [{'alg': 'SHA-256', 'content': lib_hash}],
|
||||
'externalReferences': [
|
||||
{'type': 'vcs',
|
||||
'url': 'https://github.com/wolfSSL/wolfssl'},
|
||||
'url': urls['vcs']},
|
||||
{'type': 'website',
|
||||
'url': 'https://www.wolfssl.com/'},
|
||||
{'type': 'issue-tracker',
|
||||
'url': 'https://github.com/wolfSSL/wolfssl/issues'},
|
||||
'url': urls['issues']},
|
||||
{'type': 'advisories',
|
||||
'url': 'https://github.com/wolfSSL/wolfssl/security/advisories'},
|
||||
'url': urls['advisories']},
|
||||
{'type': 'security-contact',
|
||||
'url': 'https://www.wolfssl.com/.well-known/security.txt'},
|
||||
],
|
||||
@@ -884,12 +932,19 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
if srcs_basenames:
|
||||
_annotate('wolfssl:sbom:source-set=' + ','.join(srcs_basenames))
|
||||
|
||||
urls = project_urls(name)
|
||||
# Main-package SPDXID derived from --name (sanitised per SPDX 2.3 idstring
|
||||
# rules) rather than hardcoded to wolfssl, so a wolfSSH/wolfMQTT SBOM does
|
||||
# not mislabel its own package as wolfssl. For name='wolfssl' the result
|
||||
# is 'SPDXRef-Package-wolfssl', unchanged from before.
|
||||
main_spdx_id = 'SPDXRef-Package-' + re.sub(r'[^A-Za-z0-9.]', '', name)
|
||||
|
||||
wolfssl_pkg = {
|
||||
'SPDXID': 'SPDXRef-Package-wolfssl',
|
||||
'SPDXID': main_spdx_id,
|
||||
'name': name,
|
||||
'versionInfo': version,
|
||||
'supplier': f'Organization: {supplier}',
|
||||
'downloadLocation': 'https://github.com/wolfSSL/wolfssl',
|
||||
'downloadLocation': urls['vcs'],
|
||||
'filesAnalyzed': False,
|
||||
'checksums': [{'algorithm': 'SHA256', 'checksumValue': lib_hash}],
|
||||
'licenseConcluded': license_id,
|
||||
@@ -913,9 +968,7 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
{
|
||||
'referenceCategory': 'SECURITY',
|
||||
'referenceType': 'advisory',
|
||||
'referenceLocator': (
|
||||
'https://github.com/wolfSSL/wolfssl/security/advisories'
|
||||
),
|
||||
'referenceLocator': urls['advisories'],
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -941,7 +994,7 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
packages = [wolfssl_pkg]
|
||||
relationships = [{
|
||||
'spdxElementId': 'SPDXRef-DOCUMENT',
|
||||
'relatedSpdxElement': 'SPDXRef-Package-wolfssl',
|
||||
'relatedSpdxElement': main_spdx_id,
|
||||
'relationshipType': 'DESCRIBES',
|
||||
}]
|
||||
|
||||
@@ -949,7 +1002,7 @@ def generate_spdx(name, version, supplier, license_id, license_text, lib_hash,
|
||||
spdx_id, pkg = spdx_dep_package(key, dep_version_overrides)
|
||||
packages.append(pkg)
|
||||
relationships.append({
|
||||
'spdxElementId': 'SPDXRef-Package-wolfssl',
|
||||
'spdxElementId': main_spdx_id,
|
||||
'relatedSpdxElement': spdx_id,
|
||||
'relationshipType': 'DEPENDS_ON',
|
||||
})
|
||||
@@ -1102,6 +1155,13 @@ def main():
|
||||
'directing integrators to contact wolfSSL. '
|
||||
'Mutually exclusive with --lib / --srcs / '
|
||||
'--srcs-file.')
|
||||
parser.add_argument('--dep-wolfssl', default='no',
|
||||
help='yes to record wolfssl as a dependency component '
|
||||
'(for downstream wolfSSL-stack products such as '
|
||||
'wolfSSH / wolfMQTT that link libwolfssl). '
|
||||
'wolfSSL\'s own SBOM leaves this off. Combine '
|
||||
'with --dep-version wolfssl=X.Y.Z on hosts '
|
||||
'without wolfssl.pc.')
|
||||
parser.add_argument('--dep-libz', default='no',
|
||||
help='yes if built with --with-libz')
|
||||
parser.add_argument('--dep-liboqs', default='no',
|
||||
@@ -1173,8 +1233,9 @@ def main():
|
||||
|
||||
enabled_deps = [
|
||||
key for key, flag in [
|
||||
('libz', args.dep_libz),
|
||||
('liboqs', args.dep_liboqs),
|
||||
('wolfssl', args.dep_wolfssl),
|
||||
('libz', args.dep_libz),
|
||||
('liboqs', args.dep_liboqs),
|
||||
]
|
||||
if flag.lower() == 'yes'
|
||||
]
|
||||
|
||||
@@ -365,6 +365,50 @@ class TestDetectLicense(unittest.TestCase):
|
||||
'License version 3, or any later version.\n'),
|
||||
'GPL-3.0-or-later')
|
||||
|
||||
def test_gplv3_abbreviation_only(self):
|
||||
# LICENSING that uses only the "GPLv3" abbreviation, with no
|
||||
# canonical "GNU General Public License version 3" long form.
|
||||
# This is exactly wolfSSH's LICENSING shape, which previously
|
||||
# fell back to NOASSERTION. Oracle: 'GPL-3.0-only'.
|
||||
self.assertEqual(
|
||||
self._detect(
|
||||
'wolfExample is either licensed for use under the GPLv3 '
|
||||
'or a standard commercial license.\n'),
|
||||
'GPL-3.0-only')
|
||||
|
||||
def test_gplv2_abbreviation_only(self):
|
||||
# Same abbreviation path for version 2. Oracle: 'GPL-2.0-only'.
|
||||
self.assertEqual(
|
||||
self._detect('Distributed under the GPLv2.\n'),
|
||||
'GPL-2.0-only')
|
||||
|
||||
def test_gplv3_plus_abbreviation_is_or_later(self):
|
||||
# The "+" suffix on the abbreviated form (GPLv3+) denotes the
|
||||
# or-later variant. Oracle: 'GPL-3.0-or-later'.
|
||||
self.assertEqual(
|
||||
self._detect('Licensed under GPLv3+ terms.\n'),
|
||||
'GPL-3.0-or-later')
|
||||
|
||||
def test_gplv2_abbreviation_or_later_prose(self):
|
||||
# Abbreviated form followed by an explicit "or later" clause in
|
||||
# prose (no "+") also promotes to or-later. Oracle:
|
||||
# 'GPL-2.0-or-later'.
|
||||
self.assertEqual(
|
||||
self._detect('Available under GPLv2 or later.\n'),
|
||||
'GPL-2.0-or-later')
|
||||
|
||||
def test_real_wolfssh_licensing_shape_is_gpl3_only(self):
|
||||
# Regression guard for the exact wolfSSH LICENSING wording: the
|
||||
# "or a standard commercial license" clause after the GPLv3
|
||||
# abbreviation must NOT be mistaken for an "or later" grant.
|
||||
self.assertEqual(
|
||||
self._detect(
|
||||
'\nwolfSSH is either licensed for use under the GPLv3 or a '
|
||||
'standard commercial\nlicense. For our users who cannot use '
|
||||
'wolfSSH under GPLv3, a commercial license\nto wolfSSH is '
|
||||
'available.\n'),
|
||||
'GPL-3.0-only')
|
||||
|
||||
def test_case_insensitive(self):
|
||||
# The regex is case-insensitive for both the GPL header line
|
||||
# and the 'or later' clause. Real-world COPYING files use
|
||||
@@ -782,8 +826,26 @@ class TestDepMetaShape(unittest.TestCase):
|
||||
* a future PR re-introducing the `falcon`/`libxmss`/`liblms`
|
||||
keys after they were intentionally removed."""
|
||||
|
||||
def test_only_libz_and_liboqs_are_tracked(self):
|
||||
self.assertEqual(set(gs.DEP_META.keys()), {'libz', 'liboqs'})
|
||||
def test_only_expected_deps_are_tracked(self):
|
||||
# wolfssl is tracked so downstream wolfSSL-stack products (wolfSSH,
|
||||
# wolfMQTT, ...) can declare it via --dep-wolfssl; libz/liboqs are
|
||||
# wolfSSL's own optional linked deps.
|
||||
self.assertEqual(set(gs.DEP_META.keys()),
|
||||
{'wolfssl', 'libz', 'liboqs'})
|
||||
|
||||
def test_wolfssl_dep_entry_describes_the_linked_artefact(self):
|
||||
wolfssl = gs.DEP_META['wolfssl']
|
||||
self.assertEqual(wolfssl['name'], 'wolfssl')
|
||||
self.assertEqual(wolfssl['supplier'], 'wolfSSL Inc.')
|
||||
self.assertEqual(wolfssl['pkgconfig'], 'wolfssl')
|
||||
# wolfSSL ships under GPLv3 (LICENSING: "version 3 (GPLv3)", no
|
||||
# "or later"); the dependency entry must match what
|
||||
# detect_license() infers for wolfSSL's own main-package SBOM so a
|
||||
# downstream product's wolfssl dep and wolfSSL's self-SBOM agree.
|
||||
self.assertEqual(wolfssl['license'], 'GPL-3.0-only')
|
||||
self.assertEqual(
|
||||
wolfssl['purl']('5.7.4'),
|
||||
'pkg:github/wolfSSL/wolfssl@v5.7.4')
|
||||
|
||||
def test_liboqs_entry_describes_the_linked_artefact(self):
|
||||
liboqs = gs.DEP_META['liboqs']
|
||||
@@ -824,6 +886,7 @@ class TestEnabledDepsCli(unittest.TestCase):
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertIn('--dep-liboqs', result.stdout)
|
||||
self.assertIn('--dep-libz', result.stdout)
|
||||
self.assertIn('--dep-wolfssl', result.stdout)
|
||||
|
||||
def test_removed_flags_are_rejected(self):
|
||||
# Each of these was either renamed (--dep-falcon -> --dep-liboqs)
|
||||
|
||||
Reference in New Issue
Block a user