Skip to content

Commit 143faa9

Browse files
authored
Import vulnerability data from ScanCode.io (#448)
Signed-off-by: tdruez <[email protected]>
1 parent f10f251 commit 143faa9

File tree

5 files changed

+137
-16
lines changed

5 files changed

+137
-16
lines changed

dje/tests/test_admin.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,14 +256,14 @@ def test_dataspace_admin_changelist_missing_in_filter_dataspace_value_validation
256256
self.assertEqual(302, response.status_code)
257257
self.assertIn("?e=1", response["Location"])
258258

259-
data = {MissingInFilter.parameter_name: 99}
259+
data = {MissingInFilter.parameter_name: 999999}
260260
response = self.client.get(url, data=data)
261261
self.assertEqual(302, response.status_code)
262262
self.assertIn("?e=1", response["Location"])
263263

264264
data = {
265265
MissingInFilter.parameter_name: self.other_dataspace.id,
266-
DataspaceFilter.parameter_name: 99,
266+
DataspaceFilter.parameter_name: 999999,
267267
}
268268
response = self.client.get(url, data=data)
269269
self.assertEqual(200, response.status_code)

product_portfolio/importers.py

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -699,9 +699,33 @@ def import_dependencies(self):
699699
for dependency_data in self.dependencies:
700700
self.import_dependency(dependency_data)
701701

702+
@staticmethod
703+
def import_vulnerability(vulnerability_data, product_package):
704+
from vulnerabilities.models import VulnerabilityAnalysis
705+
706+
package = product_package.package
707+
vulnerabilities = package.create_vulnerabilities(vulnerabilities_data=[vulnerability_data])
708+
if not vulnerabilities:
709+
return
710+
711+
if cdx_vulnerability := vulnerability_data.get("cdx_vulnerability_data"):
712+
if analysis_data := cdx_vulnerability.get("analysis"):
713+
# CycloneDX model uses "response" while the local model uses "response"
714+
if response_value := analysis_data.pop("response", None):
715+
analysis_data["responses"] = response_value
716+
717+
VulnerabilityAnalysis.create_from_data(
718+
user=product_package.dataspace,
719+
data={
720+
"product_package": product_package,
721+
"vulnerability": vulnerabilities[0],
722+
**analysis_data,
723+
},
724+
)
725+
702726
def import_package(self, package_data):
703-
# Vulnerabilities are fetched post import.
704-
package_data.pop("affected_by_vulnerabilities", None)
727+
# Vulnerabilities are assigned after the package creation.
728+
affected_by_vulnerabilities = package_data.pop("affected_by_vulnerabilities", [])
705729

706730
# Check if the package already exists to prevent duplication.
707731
package = self.look_for_existing_package(package_data)
@@ -730,7 +754,7 @@ def import_package(self, package_data):
730754
return
731755
self.created["package"].append(str(package))
732756

733-
ProductPackage.objects.get_or_create(
757+
product_package, _ = ProductPackage.objects.get_or_create(
734758
product=self.product,
735759
package=package,
736760
dataspace=self.product.dataspace,
@@ -743,6 +767,9 @@ def import_package(self, package_data):
743767
package_uid = package_data.get("package_uid") or package.uuid
744768
self.package_uid_mapping[package_uid] = package
745769

770+
for vulnerability_data in affected_by_vulnerabilities:
771+
self.import_vulnerability(vulnerability_data, product_package)
772+
746773
def import_dependency(self, dependency_data):
747774
dependency_uid = dependency_data.get("dependency_uid")
748775

product_portfolio/tests/test_importers.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1255,3 +1255,54 @@ def test_product_portfolio_import_packages_from_scio_importer_duplicate_dependen
12551255
self.assertEqual({}, errors)
12561256
self.assertEqual(2, self.product1.packages.count())
12571257
self.assertEqual(1, self.product1.dependencies.count())
1258+
1259+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_dependencies")
1260+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages")
1261+
def test_product_portfolio_import_packages_from_scio_importer_vex(
1262+
self, mock_fetch_packages, mock_fetch_dependencies
1263+
):
1264+
vulnerability_data = {
1265+
"id": "ID-0001", # In CycloneDX the field name is "id"
1266+
"summary": "complexity bugs may lead to a denial of service",
1267+
"cdx_vulnerability_data": {
1268+
"affects": [{"ref": "pkg:maven/abc/[email protected]"}],
1269+
"bom-ref": "BomRef.1",
1270+
"description": "complexity bugs may lead to a denial of service",
1271+
"analysis": {
1272+
"detail": "AAAA",
1273+
"justification": "code_not_present",
1274+
"response": ["can_not_fix", "update"],
1275+
"state": "resolved",
1276+
},
1277+
},
1278+
}
1279+
mock_fetch_packages.return_value = [
1280+
{
1281+
"purl": "pkg:maven/abc/[email protected]",
1282+
"type": "maven",
1283+
"namespace": "abc",
1284+
"name": "abc",
1285+
"version": "1.0",
1286+
"affected_by_vulnerabilities": [vulnerability_data],
1287+
}
1288+
]
1289+
1290+
importer = ImportPackageFromScanCodeIO(
1291+
user=self.super_user,
1292+
project_uuid=uuid.uuid4(),
1293+
product=self.product1,
1294+
)
1295+
created, existing, errors = importer.save()
1296+
created_package = self.product1.packages.get()
1297+
vulnerability = created_package.affected_by_vulnerabilities.get()
1298+
self.assertEqual(vulnerability_data["id"], vulnerability.vulnerability_id)
1299+
self.assertEqual(vulnerability_data["summary"], vulnerability.summary)
1300+
1301+
analysis = vulnerability.vulnerability_analyses.get()
1302+
self.assertEqual(vulnerability, analysis.vulnerability)
1303+
self.assertEqual(self.product1, analysis.product)
1304+
self.assertEqual(created_package, analysis.package)
1305+
self.assertEqual("resolved", analysis.state)
1306+
self.assertEqual("code_not_present", analysis.justification)
1307+
self.assertEqual("AAAA", analysis.detail)
1308+
self.assertEqual(["can_not_fix", "update"], analysis.responses)

vulnerabilities/models.py

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -181,13 +181,36 @@ def add_affected(self, instances):
181181

182182
@classmethod
183183
def create_from_data(cls, dataspace, data, validate=False, affecting=None):
184+
"""Create a Vulnerability from provided ``data``."""
184185
instance = super().create_from_data(user=dataspace, data=data, validate=False)
185186

186187
if affecting:
187188
instance.add_affected(affecting)
188189

189190
return instance
190191

192+
@classmethod
193+
def get_or_create_from_data(cls, dataspace, data, validate=False):
194+
"""Get or create a Vulnerability from provided ``data``."""
195+
vulnerability_qs = Vulnerability.objects.scope(dataspace)
196+
197+
# Support for CycloneDX data structure
198+
data = data.copy()
199+
vulnerability_id = data.get("vulnerability_id") or data.pop("id", None)
200+
if not vulnerability_id:
201+
return
202+
data["vulnerability_id"] = vulnerability_id
203+
204+
vulnerability = vulnerability_qs.get_or_none(vulnerability_id=vulnerability_id)
205+
if not vulnerability:
206+
vulnerability = cls.create_from_data(
207+
dataspace=dataspace,
208+
data=data,
209+
validate=validate,
210+
)
211+
212+
return vulnerability
213+
191214
def as_cyclonedx(self, affected_instances, analysis=None):
192215
affects = [
193216
cdx_vulnerability.BomTarget(ref=instance.cyclonedx_bom_ref)
@@ -465,28 +488,28 @@ def fetch_vulnerabilities(self):
465488
self.create_vulnerabilities(vulnerabilities_data=affected_by_vulnerabilities)
466489

467490
def create_vulnerabilities(self, vulnerabilities_data):
491+
"""Create and assign Vulnerabilities to this instance from provided vulnerabilities_data."""
468492
from component_catalog.models import Package
469493

470494
vulnerabilities = []
471-
vulnerability_qs = Vulnerability.objects.scope(self.dataspace)
472-
473495
for vulnerability_data in vulnerabilities_data:
474-
vulnerability_id = vulnerability_data["vulnerability_id"]
475-
vulnerability = vulnerability_qs.get_or_none(vulnerability_id=vulnerability_id)
476-
if not vulnerability:
477-
vulnerability = Vulnerability.create_from_data(
478-
dataspace=self.dataspace,
479-
data=vulnerability_data,
480-
)
496+
vulnerability = Vulnerability.get_or_create_from_data(
497+
dataspace=self.dataspace,
498+
data=vulnerability_data,
499+
)
481500
vulnerabilities.append(vulnerability)
482501

483-
through_defaults = {"dataspace_id": self.dataspace_id}
484-
self.affected_by_vulnerabilities.add(*vulnerabilities, through_defaults=through_defaults)
502+
self.affected_by_vulnerabilities.add(
503+
*vulnerabilities,
504+
through_defaults={"dataspace_id": self.dataspace_id},
505+
)
485506

486507
self.update_risk_score()
487508
if isinstance(self, Package):
488509
self.productpackages.update_weighted_risk_score()
489510

511+
return vulnerabilities
512+
490513

491514
class VulnerabilityAnalysis(
492515
VulnerabilityAnalysisMixin,

vulnerabilities/tests/test_models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,26 @@ def test_vulnerability_model_create_from_data(self):
230230
self.assertEqual(vulnerability_data["resource_url"], vulnerability1.resource_url)
231231
self.assertQuerySetEqual(vulnerability1.affected_packages.all(), [package1])
232232

233+
def test_vulnerability_model_get_or_create_from_data(self):
234+
vulnerability_data = {
235+
"id": "VCID-q4q6-yfng-aaag",
236+
"summary": "In Django 3.2 before 3.2.25, 4.2 before 4.2.11, and 5.0.",
237+
}
238+
239+
vulnerability1 = Vulnerability.get_or_create_from_data(
240+
dataspace=self.dataspace,
241+
data=vulnerability_data,
242+
)
243+
self.assertEqual(vulnerability_data["id"], vulnerability1.vulnerability_id)
244+
self.assertEqual(vulnerability_data["summary"], vulnerability1.summary)
245+
246+
vulnerability_data["vulnerability_id"] = vulnerability_data["id"]
247+
vulnerability2 = Vulnerability.get_or_create_from_data(
248+
dataspace=self.dataspace,
249+
data=vulnerability_data,
250+
)
251+
self.assertEqual(vulnerability1.id, vulnerability2.id)
252+
233253
def test_vulnerability_model_queryset_count_methods(self):
234254
package1 = make_package(self.dataspace)
235255
package2 = make_package(self.dataspace)

0 commit comments

Comments
 (0)