From 48496b3beb365ec6544f0259536436e9cdf53a50 Mon Sep 17 00:00:00 2001 From: Mark Atwood Date: Thu, 23 Jul 2026 11:37:39 -0700 Subject: [PATCH] fix(advisory): per-product component in CDX VEX The CycloneDX VEX emitter collapsed every non-FIPS product onto the hardcoded wolfssl metadata.component, so a wolfSSH (or wolfMQTT, wolfTPM, ...) CVE emitted a VEX whose affects[] pointed at a component named "wolfssl" -- disagreeing with the CSAF output for the same record and mis-attributing the vulnerability. Mint a per-product component keyed on cdx_key (as the FIPS path already does) with the product's own name/cpe/purl, and reference it in affects[]; wolfssl products still use the umbrella metadata.component. Adds a regression test class covering a wolfSSH record. --- central/gen-advisory | 16 ++++++++++++++- central/test_gen_advisory.py | 38 ++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/central/gen-advisory b/central/gen-advisory index b4c7522..ad4db0d 100755 --- a/central/gen-advisory +++ b/central/gen-advisory @@ -724,8 +724,22 @@ def generate_cdx_vex(advs, ov_map, advisory_id, timestamp): {'name': 'wolfssl:fips:cmvp', 'value': m} for m in prod['model_numbers']] extra_components[ref] = comp - else: + elif prod['cdx_key'] == 'wolfssl': ref = main_ref + else: + # Any other product (wolfSSH, wolfMQTT, wolfTPM, ...) gets its + # own correctly-named component rather than being collapsed + # onto the wolfssl metadata.component -- otherwise the CDX VEX + # attributes a non-wolfssl product's CVE to wolfssl. + ref = derived_uuid('cdx-component', prod['cdx_key']) + if ref not in extra_components: + extra_components[ref] = { + 'bom-ref': ref, 'type': 'library', + 'supplier': {'name': 'wolfSSL Inc.'}, + 'name': prod['product_name'], + 'cpe': cpe_for(prod['product_name'], '*'), + 'purl': f'pkg:github/wolfSSL/{prod["cdx_key"]}', + } # CycloneDX affects[].versions[].status uses 'unaffected' for a # not-affected product; the 'not_affected' term belongs to # analysis.state only. diff --git a/central/test_gen_advisory.py b/central/test_gen_advisory.py index 98ee7f3..f7b8db9 100755 --- a/central/test_gen_advisory.py +++ b/central/test_gen_advisory.py @@ -565,6 +565,44 @@ def test_analysis_state_and_cwe_and_rating(self): self.assertEqual(v['ratings'][0]['severity'], 'critical') +class TestCdxVexNonWolfsslProduct(unittest.TestCase): + """Regression: a non-wolfssl product's CVE must be attributed to its own + component, not collapsed onto the wolfssl metadata.component (which would + make the CDX VEX say a wolfSSH bug is a wolfssl bug).""" + + def setUp(self): + rec = { + 'cveMetadata': {'cveId': 'CVE-2026-9001'}, + 'containers': {'cna': { + 'descriptions': [{'lang': 'en', 'value': 'A wolfSSH issue.'}], + 'affected': [{'vendor': 'wolfSSL', 'product': 'wolfSSH', + 'versions': [{'version': '0', + 'lessThanOrEqual': '1.4.19', + 'status': 'affected'}]}], + }}, + } + self.bom = ga.generate_cdx_vex([ga.parse_record(rec)], {}, + 'wolfSSH-SA-1', PINNED_EPOCH_ISO) + + def test_metadata_component_stays_wolfssl_umbrella(self): + self.assertEqual(self.bom['metadata']['component']['name'], 'wolfssl') + + def test_product_gets_its_own_component(self): + comps = {c['name']: c for c in self.bom['components']} + self.assertIn('wolfSSH', comps) + self.assertEqual(comps['wolfSSH']['purl'], 'pkg:github/wolfSSL/wolfssh') + self.assertEqual(comps['wolfSSH']['cpe'], + 'cpe:2.3:a:wolfssl:wolfssh:*:*:*:*:*:*:*:*') + + def test_affects_points_to_product_not_wolfssl(self): + wolfssh_ref = next(c['bom-ref'] for c in self.bom['components'] + if c['name'] == 'wolfSSH') + main_ref = self.bom['metadata']['component']['bom-ref'] + refs = {a['ref'] for a in self.bom['vulnerabilities'][0]['affects']} + self.assertIn(wolfssh_ref, refs) + self.assertNotIn(main_ref, refs) + + # --------------------------------------------------------------------------- # # Overlay matches its own schema vocabulary (lightweight, no jsonschema). # The authoritative jsonschema pass runs in CI; this guards the committed