From 2066202abe9c25d49ccce3d8fdde762ffe60968d Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 31 Dec 2025 17:35:09 +0400 Subject: [PATCH 1/2] Import vulnerability data from ScanCode.io Signed-off-by: tdruez --- product_portfolio/importers.py | 15 ++++++-- product_portfolio/tests/test_importers.py | 44 +++++++++++++++++++++++ 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/product_portfolio/importers.py b/product_portfolio/importers.py index 7b931bcf..07a52c6b 100644 --- a/product_portfolio/importers.py +++ b/product_portfolio/importers.py @@ -699,9 +699,17 @@ def import_dependencies(self): for dependency_data in self.dependencies: self.import_dependency(dependency_data) + @staticmethod + def import_vulnerability(vulnerability_data, package): + vulnerability_id = vulnerability_data.get("vulnerability_id") + if not vulnerability_id: + return + + package.create_vulnerabilities(vulnerabilities_data=[vulnerability_data]) + def import_package(self, package_data): - # Vulnerabilities are fetched post import. - package_data.pop("affected_by_vulnerabilities", None) + # Vulnerabilities are assigned after the package creation. + affected_by_vulnerabilities = package_data.pop("affected_by_vulnerabilities", []) # Check if the package already exists to prevent duplication. package = self.look_for_existing_package(package_data) @@ -743,6 +751,9 @@ def import_package(self, package_data): package_uid = package_data.get("package_uid") or package.uuid self.package_uid_mapping[package_uid] = package + for vulnerability_data in affected_by_vulnerabilities: + self.import_vulnerability(vulnerability_data, package) + def import_dependency(self, dependency_data): dependency_uid = dependency_data.get("dependency_uid") diff --git a/product_portfolio/tests/test_importers.py b/product_portfolio/tests/test_importers.py index 6ee6f9fd..f99fc14a 100644 --- a/product_portfolio/tests/test_importers.py +++ b/product_portfolio/tests/test_importers.py @@ -1255,3 +1255,47 @@ def test_product_portfolio_import_packages_from_scio_importer_duplicate_dependen self.assertEqual({}, errors) self.assertEqual(2, self.product1.packages.count()) self.assertEqual(1, self.product1.dependencies.count()) + + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_dependencies") + @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages") + def test_product_portfolio_import_packages_from_scio_importer_vex( + self, mock_fetch_packages, mock_fetch_dependencies + ): + vulnerability_data = { + # "id": "VCID-0001", + "vulnerability_id": "VCID-0001", + "summary": "complexity bugs may lead to a denial of service", + "cdx_vulnerability": { + "affects": [{"ref": "pkg:maven/abc/abc@1.0"}], + "bom-ref": "BomRef.1", + "description": "complexity bugs may lead to a denial of service", + "analysis": { + "detail": "AAAA", + "justification": "code_not_present", + "response": ["can_not_fix", "update"], + "state": "resolved", + }, + }, + } + mock_fetch_packages.return_value = [ + { + "purl": "pkg:maven/abc/abc@1.0", + "type": "maven", + "namespace": "abc", + "name": "abc", + "version": "1.0", + "affected_by_vulnerabilities": [vulnerability_data], + } + ] + + importer = ImportPackageFromScanCodeIO( + user=self.super_user, + project_uuid=uuid.uuid4(), + product=self.product1, + infer_download_urls=True, + ) + created, existing, errors = importer.save() + created_package = self.product1.packages.get() + vulnerability = created_package.affected_by_vulnerabilities.get() + self.assertEqual(vulnerability_data["vulnerability_id"], vulnerability.vulnerability_id) + self.assertEqual(vulnerability_data["summary"], vulnerability.summary) From b870de3429667e667d63286cfff2068599e6505c Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 31 Dec 2025 17:47:11 +0400 Subject: [PATCH 2/2] Import VEX data from ScanCode.io into VulnerabilityAnalysis model Signed-off-by: tdruez --- product_portfolio/importers.py | 24 +++++++++++++++++++---- product_portfolio/tests/test_importers.py | 11 ++++++++++- vulnerabilities/models.py | 2 ++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/product_portfolio/importers.py b/product_portfolio/importers.py index 07a52c6b..5fd307fb 100644 --- a/product_portfolio/importers.py +++ b/product_portfolio/importers.py @@ -700,12 +700,28 @@ def import_dependencies(self): self.import_dependency(dependency_data) @staticmethod - def import_vulnerability(vulnerability_data, package): + def import_vulnerability(vulnerability_data, product_package): + from vulnerabilities.models import VulnerabilityAnalysis + vulnerability_id = vulnerability_data.get("vulnerability_id") if not vulnerability_id: return - package.create_vulnerabilities(vulnerabilities_data=[vulnerability_data]) + package = product_package.package + # TODO: Add a lower-level create_vulnerability method + vulnerabilities = package.create_vulnerabilities(vulnerabilities_data=[vulnerability_data]) + vulnerability = vulnerabilities[0] + + if cdx_vulnerability := vulnerability_data.get("cdx_vulnerability"): + if analysis_data := cdx_vulnerability.get("analysis"): + VulnerabilityAnalysis.create_from_data( + user=product_package.dataspace, + data={ + "product_package": product_package, + "vulnerability": vulnerability, + **analysis_data, + }, + ) def import_package(self, package_data): # Vulnerabilities are assigned after the package creation. @@ -738,7 +754,7 @@ def import_package(self, package_data): return self.created["package"].append(str(package)) - ProductPackage.objects.get_or_create( + product_package, _ = ProductPackage.objects.get_or_create( product=self.product, package=package, dataspace=self.product.dataspace, @@ -752,7 +768,7 @@ def import_package(self, package_data): self.package_uid_mapping[package_uid] = package for vulnerability_data in affected_by_vulnerabilities: - self.import_vulnerability(vulnerability_data, package) + self.import_vulnerability(vulnerability_data, product_package) def import_dependency(self, dependency_data): dependency_uid = dependency_data.get("dependency_uid") diff --git a/product_portfolio/tests/test_importers.py b/product_portfolio/tests/test_importers.py index f99fc14a..05a39796 100644 --- a/product_portfolio/tests/test_importers.py +++ b/product_portfolio/tests/test_importers.py @@ -1272,7 +1272,7 @@ def test_product_portfolio_import_packages_from_scio_importer_vex( "analysis": { "detail": "AAAA", "justification": "code_not_present", - "response": ["can_not_fix", "update"], + "responses": ["can_not_fix", "update"], "state": "resolved", }, }, @@ -1299,3 +1299,12 @@ def test_product_portfolio_import_packages_from_scio_importer_vex( vulnerability = created_package.affected_by_vulnerabilities.get() self.assertEqual(vulnerability_data["vulnerability_id"], vulnerability.vulnerability_id) self.assertEqual(vulnerability_data["summary"], vulnerability.summary) + + analysis = vulnerability.vulnerability_analyses.get() + self.assertEqual(vulnerability, analysis.vulnerability) + self.assertEqual(self.product1, analysis.product) + self.assertEqual(created_package, analysis.package) + self.assertEqual("resolved", analysis.state) + self.assertEqual("code_not_present", analysis.justification) + self.assertEqual("AAAA", analysis.detail) + self.assertEqual(["can_not_fix", "update"], analysis.responses) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 456c9b74..25df6a41 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -487,6 +487,8 @@ def create_vulnerabilities(self, vulnerabilities_data): if isinstance(self, Package): self.productpackages.update_weighted_risk_score() + return vulnerabilities + class VulnerabilityAnalysis( VulnerabilityAnalysisMixin,