@@ -443,3 +443,93 @@ class SPDX30Check(SPDX3CheckBase, OESelftestTestCase):
r'\d',
f"Version '{version}' for package '{name}' should contain digits"
)
+
+ def test_openvex_integration(self):
+ """
+ Test that OpenVEX generation is integrated into SPDX workflow.
+
+ Verifies VEX relationships are created for vulnerabilities and
+ packages have PURLs suitable for VEX product identification.
+ """
+ objset = self.check_recipe_spdx(
+ "busybox",
+ "{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/static/static-busybox.spdx.json",
+ task="create_recipe_spdx",
+ extraconf=""" OPENVEX_AUTHOR = "Test Author"
+ OPENVEX_ROLE = "securityAdvisor"
+ """,
+ )
+
+ # Check for VEX relationships (any type: Fixed, Affected, NotAffected, etc.)
+ vex_count = 0
+ for rel in objset.foreach_type(oe.spdx30.security_VexVulnAssessmentRelationship):
+ vex_count += 1
+ self.assertIsNotNone(rel.from_, "VEX relationship missing 'from' field")
+ self.assertIsNotNone(rel.to, "VEX relationship missing 'to' field")
+
+ if vex_count:
+ self.logger.info(f"Found {vex_count} VEX relationships in SPDX")
+ else:
+ self.logger.info("No VEX relationships found (expected if no CVEs)")
+
+ # Verify packages have PURLs for VEX product identification
+ packages_with_purls = []
+ for pkg in objset.foreach_type(oe.spdx30.software_Package):
+ if hasattr(pkg, "externalIdentifier") and pkg.externalIdentifier:
+ for ext_id in pkg.externalIdentifier:
+ if hasattr(ext_id, "externalIdentifierType"):
+ if "packageurl" in str(ext_id.externalIdentifierType).lower():
+ packages_with_purls.append(pkg.name)
+ break
+
+ self.assertGreater(
+ len(packages_with_purls), 0,
+ "Should have packages with PURLs for VEX product identification"
+ )
+ self.logger.info(f"Found {len(packages_with_purls)} packages with PURLs")
+
+ def test_openvex_standalone_files(self):
+ """
+ Test that standalone OpenVEX files are generated when enabled.
+
+ Verifies OpenVEX JSON files are created with required metadata
+ for a recipe with known CVEs (busybox).
+ """
+ import json
+ from pathlib import Path
+
+ self.check_recipe_spdx(
+ "busybox",
+ "{DEPLOY_DIR_SPDX}/{SSTATE_PKGARCH}/static/static-busybox.spdx.json",
+ task="create_recipe_spdx",
+ extraconf=""" OPENVEX_GENERATE_STANDALONE = "1"
+ OPENVEX_AUTHOR = "Test Security Team"
+ OPENVEX_ROLE = "securityAdvisor"
+ """,
+ )
+
+ deploy_dir_spdx = get_bb_var("DEPLOY_DIR_SPDX")
+ sstate_pkgarch = get_bb_var("SSTATE_PKGARCH", "busybox")
+
+ vex_file = Path(deploy_dir_spdx) / sstate_pkgarch / "recipes" / "busybox.vex.json"
+
+ self.assertExists(str(vex_file), "busybox.vex.json should exist (busybox has known CVEs)")
+
+ with open(vex_file, "r") as f:
+ vex_data = json.load(f)
+
+ self.assertIn("@context", vex_data, "VEX missing @context")
+ self.assertIn("statements", vex_data, "VEX missing statements")
+ self.assertGreater(len(vex_data["statements"]), 0, "VEX should have at least one statement")
+
+ self.assertEqual(
+ vex_data["author"],
+ "Test Security Team",
+ "VEX author not set correctly"
+ )
+
+ self.logger.info(
+ f"Validated OpenVEX file: busybox.vex.json "
+ f"({len(vex_data['statements'])} statements)"
+ )
+