Skip to content

Commit

Permalink
Add the ability to download the VEX output #108
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez committed Sep 3, 2024
1 parent 27db675 commit 75c6c26
Show file tree
Hide file tree
Showing 8 changed files with 87 additions and 16 deletions.
6 changes: 4 additions & 2 deletions component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2750,8 +2750,10 @@ def get_severity_scores(severities):

return consolidated_scores

def as_cyclonedx(self, component_bom_ref):
affects = [cdx_vulnerability.BomTarget(ref=f"urn:cdx:{component_bom_ref}")]
def as_cyclonedx(self, affected_instances):
affects = [
cdx_vulnerability.BomTarget(ref=str(instance.uuid)) for instance in affected_instances
]

source_url = f"https://public.vulnerablecode.io/vulnerabilities/{self.vulnerability_id}"
source = cdx_vulnerability.VulnerabilitySource(
Expand Down
3 changes: 2 additions & 1 deletion component_catalog/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -2743,8 +2743,9 @@ def test_vulnerability_model_as_cyclonedx(self):
dataspace=self.dataspace,
data=affected_by_vulnerabilities[0],
)
package1 = make_package(self.dataspace, uuid="dd0afd00-89bd-46d6-b1f0-57b553c44d32")

vulnerability1_as_cdx = vulnerability1.as_cyclonedx(component_bom_ref="ref")
vulnerability1_as_cdx = vulnerability1.as_cyclonedx(affected_instances=[package1])
as_dict = json.loads(vulnerability1_as_cdx.as_json())
as_dict.pop("ratings", None) # The sorting is inconsistent
results = json.dumps(as_dict, indent=2)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"affects": [
{
"ref": "urn:cdx:ref"
"ref": "dd0afd00-89bd-46d6-b1f0-57b553c44d32"
}
],
"description": "Internationalized Domain Names in Applications (IDNA) vulnerable to denial of service from specially crafted inputs to idna.encode",
Expand Down
26 changes: 19 additions & 7 deletions dje/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,11 @@ def get_spdx_filename(spdx_document):
return safe_filename(filename)


def get_cyclonedx_bom(instance, user):
"""https://cyclonedx.org/use-cases/#dependency-graph"""
def get_cyclonedx_bom(instance, user, include_components=True, include_vex=False):
"""
https://cyclonedx.org/use-cases/#dependency-graph
https://cyclonedx.org/use-cases/#vulnerability-exploitability
"""
root_component = instance.as_cyclonedx()

bom = cyclonedx_bom.Bom()
Expand All @@ -122,9 +125,18 @@ def get_cyclonedx_bom(instance, user):
component.as_cyclonedx() for component in instance.get_cyclonedx_components()
]

for component in cyclonedx_components:
bom.components.add(component)
bom.register_dependency(root_component, [component])
if include_components:
for component in cyclonedx_components:
bom.components.add(component)
bom.register_dependency(root_component, [component])

if include_vex:
vulnerability_qs = instance.get_vulnerability_qs(prefetch_related_packages=True)
vulnerabilities = [
vulnerability.as_cyclonedx(affected_instances=vulnerability.affected_packages.all())
for vulnerability in vulnerability_qs
]
bom.vulnerabilities = vulnerabilities

return bom

Expand Down Expand Up @@ -165,7 +177,7 @@ def sort_bom_with_schema_ordering(bom_as_dict, schema_version):
return json.dumps(ordered_dict, indent=2)


def get_cyclonedx_filename(instance):
def get_cyclonedx_filename(instance, extension="cdx"):
base_filename = f"dejacode_{instance.dataspace.name}_{instance._meta.model_name}"
filename = f"{base_filename}_{instance}.cdx.json"
filename = f"{base_filename}_{instance}.{extension}.json"
return safe_filename(filename)
20 changes: 17 additions & 3 deletions dje/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2381,8 +2381,22 @@ class ExportCycloneDXBOMView(
def get(self, request, *args, **kwargs):
instance = self.get_object()
spec_version = self.request.GET.get("spec_version")

cyclonedx_bom = outputs.get_cyclonedx_bom(instance, self.request.user)
content = self.request.GET.get("content", "sbom")

extension = "cdx"
include_components = True
include_vex = False
if content == "vex":
extension = "vex"
include_components = False
include_vex = True

cyclonedx_bom = outputs.get_cyclonedx_bom(
instance,
self.request.user,
include_components,
include_vex,
)

try:
cyclonedx_bom_json = outputs.get_cyclonedx_bom_json(cyclonedx_bom, spec_version)
Expand All @@ -2391,6 +2405,6 @@ def get(self, request, *args, **kwargs):

return outputs.get_attachment_response(
file_content=cyclonedx_bom_json,
filename=outputs.get_cyclonedx_filename(instance),
filename=outputs.get_cyclonedx_filename(instance, extension),
content_type="application/json",
)
10 changes: 8 additions & 2 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,15 @@ def fetch_vulnerabilities(self):
"""Fetch and update the vulnerabilties of all the Package of this Product."""
return fetch_for_queryset(self.all_packages, self.dataspace)

def get_vulnerability_qs(self):
def get_vulnerability_qs(self, prefetch_related_packages=False):
"""Return a QuerySet of all Vulnerability instances related to this product"""
return Vulnerability.objects.filter(affected_packages__in=self.packages.all())
qs = Vulnerability.objects.filter(affected_packages__in=self.packages.all())

if prefetch_related_packages:
package_qs = Package.objects.filter(product=self).only_rendering_fields()
qs = qs.prefetch_related(models.Prefetch("affected_packages", package_qs))

return qs


class ProductRelationStatus(BaseStatusMixin, DataspacedModel):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@
<a class="badge text-bg-secondary" href="{{ object.get_export_cyclonedx_url }}?spec_version=1.5">1.5</a>
<a class="badge text-bg-secondary" href="{{ object.get_export_cyclonedx_url }}?spec_version=1.4">1.4</a>
</div>
{% if request.user.dataspace.enable_vulnerablecodedb_access %}
<div class="dropdown-item">
<i class="fas fa-download"></i> CycloneDX VEX
<a class="badge text-bg-primary" href="{{ object.get_export_cyclonedx_url }}?spec_version=1.6&content=vex">1.6</a>
<a class="badge text-bg-secondary" href="{{ object.get_export_cyclonedx_url }}?spec_version=1.5&content=vex">1.5</a>
<a class="badge text-bg-secondary" href="{{ object.get_export_cyclonedx_url }}?spec_version=1.4&content=vex">1.4</a>
</div>
{% endif %}
</div>
</div>
</div>
Expand Down
28 changes: 28 additions & 0 deletions product_portfolio/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2728,6 +2728,28 @@ def test_product_portfolio_product_export_spdx_get_spdx_extracted_licenses(self)
]
self.assertEqual(expected, extracted_licenses_as_dict)

def test_product_portfolio_product_details_view_cyclonedx_links(self):
self.client.login(username=self.super_user.username, password="secret")
url = self.product1.get_absolute_url()

export_cyclonedx_url = self.product1.get_export_cyclonedx_url()
sbom_link = f"{export_cyclonedx_url}?spec_version=1.6"
vex_link = f"{export_cyclonedx_url}?spec_version=1.6&content=vex"

response = self.client.get(url)
self.assertContains(response, "CycloneDX SBOM")
self.assertContains(response, sbom_link)
self.assertNotContains(response, "CycloneDX VEX")
self.assertNotContains(response, vex_link)

self.dataspace.enable_vulnerablecodedb_access = True
self.dataspace.save()
response = self.client.get(url)
self.assertContains(response, "CycloneDX SBOM")
self.assertContains(response, sbom_link)
self.assertContains(response, "CycloneDX VEX")
self.assertContains(response, vex_link)

def test_product_portfolio_product_export_cyclonedx_view(self):
owner1 = Owner.objects.create(name="Owner1", dataspace=self.dataspace)
license1 = License.objects.create(
Expand Down Expand Up @@ -2773,6 +2795,7 @@ def test_product_portfolio_product_export_cyclonedx_view(self):
ProductPackage.objects.create(
product=self.product1, package=package, dataspace=self.dataspace
)
vulnerability1 = make_vulnerability(self.dataspace, affecting=[package])

self.client.login(username=self.super_user.username, password="secret")
export_cyclonedx_url = self.product1.get_export_cyclonedx_url()
Expand Down Expand Up @@ -2853,6 +2876,11 @@ def test_product_portfolio_product_export_cyclonedx_view(self):
response = self.client.get(export_cyclonedx_url, data={"spec_version": "10.10"})
self.assertEqual(404, response.status_code)

response = self.client.get(export_cyclonedx_url, data={"content": "vex"})
response_str = str(response.getvalue())
self.assertIn("vulnerabilities", response_str)
self.assertIn(vulnerability1.vulnerability_id, response_str)

@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project")
def test_scancodeio_submit_project_task(self, mock_submit_project):
scancodeproject = ScanCodeProject.objects.create(
Expand Down

0 comments on commit 75c6c26

Please sign in to comment.