Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion dje/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,6 @@ def cyclonedx_sbom(self, request, uuid):

return outputs.get_attachment_response(
file_content=cyclonedx_bom_json,
filename=outputs.get_filename(instance, extension="cdx"),
filename=outputs.get_filename(instance, extension="cdx.json"),
content_type="application/json",
)
34 changes: 22 additions & 12 deletions dje/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

from django.http import FileResponse
from django.http import Http404
from django.utils import timezone

import msgspec
from cyclonedx import output as cyclonedx_output
Expand All @@ -33,6 +34,27 @@ def safe_filename(filename):
return re.sub("[^A-Za-z0-9.-]+", "_", filename).lower()


def get_filename(instance, extension):
filename = f"dejacode_{instance.dataspace.name}_{instance}.{extension}"
return safe_filename(filename)


def get_export_filename(dataspace, report_type, extension, instance=None):
"""Return a safe filename for exports."""
timestamp = timezone.now().strftime("%Y-%m-%d_%H%M%S")
if instance:
filename = f"dejacode_{dataspace.name}_{instance}_{report_type}_{timestamp}.{extension}"
else:
filename = f"dejacode_{dataspace.name}_{report_type}_{timestamp}.{extension}"
return safe_filename(filename)


def get_spdx_filename(spdx_document):
document_name = spdx_document.as_dict()["name"]
filename = f"{document_name}.spdx.json"
return safe_filename(filename)


def get_attachment_response(file_content, filename, content_type):
if not file_content or not filename:
raise Http404
Expand Down Expand Up @@ -97,12 +119,6 @@ def get_spdx_document(instance, user):
return document


def get_spdx_filename(spdx_document):
document_name = spdx_document.as_dict()["name"]
filename = f"{document_name}.spdx.json"
return safe_filename(filename)


def get_cyclonedx_bom(instance, user, include_components=True, include_vex=False):
"""
https://cyclonedx.org/use-cases/#dependency-graph
Expand Down Expand Up @@ -195,12 +211,6 @@ def sort_bom_with_schema_ordering(bom_as_dict, schema_version):
return json.dumps(ordered_dict, indent=2)


def get_filename(instance, extension):
base_filename = f"dejacode_{instance.dataspace.name}_{instance._meta.model_name}"
filename = f"{base_filename}_{instance}.{extension}.json"
return safe_filename(filename)


CDX_STATE_TO_CSAF_STATUS = {
"resolved": "fixed",
"resolved_with_pedigree": "fixed",
Expand Down
30 changes: 28 additions & 2 deletions dje/tests/test_outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,10 +191,36 @@ def test_outputs_get_cyclonedx_bom_json(self):

def test_outputs_get_filename(self):
self.assertEqual(
"dejacode_nexb_product_product1_with_space_1.0.cdx.json",
outputs.get_filename(instance=self.product1, extension="cdx"),
"dejacode_nexb_product1_with_space_1.0.cdx.json",
outputs.get_filename(instance=self.product1, extension="cdx.json"),
)

def test_outputs_get_export_filename(self):
mock_now = datetime(2024, 10, 10, 12, 0, 0, tzinfo=UTC)
with mock.patch("dje.outputs.timezone") as mock_timezone:
mock_timezone.now.return_value = mock_now

filename_without_instance = outputs.get_export_filename(
dataspace=self.dataspace,
report_type="vulnerabilities",
extension="csv",
)
self.assertEqual(
"dejacode_nexb_vulnerabilities_2024-10-10_120000.csv",
filename_without_instance,
)

filename_with_instance = outputs.get_export_filename(
dataspace=self.dataspace,
report_type="vulnerabilities",
extension="csv",
instance=self.product1,
)
self.assertEqual(
"dejacode_nexb_product1_with_space_1.0_vulnerabilities_2024-10-10_120000.csv",
filename_with_instance,
)

def test_outputs_get_csaf_security_advisory(self):
mock_now = datetime(2024, 12, 19, 12, 0, 0, tzinfo=UTC)
with mock.patch("dje.outputs.datetime") as mock_datetime:
Expand Down
8 changes: 4 additions & 4 deletions dje/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -2407,11 +2407,11 @@ def get(self, request, *args, **kwargs):
spec_version = self.request.GET.get("spec_version")
content = self.request.GET.get("content", "sbom")

extension = "cdx"
extension = "cdx.json"
include_components = True
include_vex = False
if content == "vex":
extension = "vex"
extension = "vex.json"
include_components = False
include_vex = True
elif content == "combined":
Expand Down Expand Up @@ -2446,7 +2446,7 @@ def get(self, request, *args, **kwargs):
product = self.get_object()
security_advisory = outputs.get_csaf_security_advisory(product)
security_advisory_json = security_advisory.model_dump_json(indent=2, exclude_none=True)
filename = outputs.get_filename(product, extension="csaf.vex")
filename = outputs.get_filename(product, extension="csaf.vex.json")

return outputs.get_attachment_response(
file_content=security_advisory_json,
Expand All @@ -2464,7 +2464,7 @@ class ExportOpenVEXView(
def get(self, request, *args, **kwargs):
product = self.get_object()
openvex_document_json = outputs.get_openvex_document_json(product)
filename = outputs.get_filename(product, extension="openvex")
filename = outputs.get_filename(product, extension="openvex.json")

return outputs.get_attachment_response(
file_content=openvex_document_json,
Expand Down
6 changes: 6 additions & 0 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,12 @@ def get_export_csaf_url(self):
def get_export_openvex_url(self):
return self.get_url("export_openvex")

def get_export_license_compliance_url(self):
return self.get_url("export_license_compliance")

def get_export_security_compliance_url(self):
return self.get_url("export_security_compliance")

@property
def cyclonedx_bom_ref(self):
return str(self.uuid)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,49 @@
<div class="border rounded-3 p-3 h-100">
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="fs-6 fw-medium mb-0">{% trans "License compliance" %}</h3>
{% if license_issues_count == 0 %}
<span class="badge text-bg-success">
{% trans "OK" %}
</span>
{% elif license_error_count > 0 %}
<span class="badge text-bg-danger">
{% trans "Error" %}
</span>
{% else %}
<span class="badge text-bg-warning">
{% trans "Warning" %}
</span>
{% endif %}
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="dropdown">
<button class="btn btn-sm btn-link text-body-secondary p-0" data-bs-toggle="dropdown">
<i class="fas fa-download"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% with export_license_compliance_url=product.get_export_license_compliance_url %}
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=csv">
<i class="fas fa-download me-1"></i>{% trans "Comma-separated Values (.csv)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=json">
<i class="fas fa-download me-1"></i>{% trans "JSON (.json)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=ods">
<i class="fas fa-download me-1"></i>{% trans "OpenDocument (.ods)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=xlsx">
<i class="fas fa-download me-1"></i>{% trans "Microsoft Excel (.xlsx)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_license_compliance_url }}?export=yaml">
<i class="fas fa-download me-1"></i>{% trans "YAML (.yaml)" %}
</a>
</li>
{% endwith %}
</ul>
</div>
{% if license_issues_count == 0 %}
<span class="badge text-bg-success">{% trans "OK" %}</span>
{% elif license_error_count > 0 %}
<span class="badge text-bg-danger">{% trans "Error" %}</span>
{% else %}
<span class="badge text-bg-warning">{% trans "Warning" %}</span>
{% endif %}
</div>
</div>

<p class="text-secondary small mb-2">
Expand Down Expand Up @@ -73,17 +103,53 @@ <h3 class="fs-6 fw-medium mb-0">{% trans "License compliance" %}</h3>
{# Header #}
<div class="d-flex justify-content-between align-items-center mb-3">
<h3 class="fs-6 fw-medium mb-0">{% trans "Security compliance" %}</h3>
{% if vulnerability_count == 0 or above_threshold_count == 0 %}
<span class="badge text-bg-success">{% trans "OK" %}</span>
{% elif max_vulnerability_severity == "critical" %}
<span class="badge text-bg-danger">{% trans "Critical" %}</span>
{% elif max_vulnerability_severity == "high" %}
<span class="badge bg-warning-orange">{% trans "High" %}</span>
{% elif max_vulnerability_severity == "medium" %}
<span class="badge text-bg-warning">{% trans "Medium" %}</span>
{% else %}
<span class="badge text-bg-info">{% trans "Low" %}</span>
{% endif %}
<div class="d-flex align-items-center gap-2 ms-auto">
<div class="dropdown">
<button class="btn btn-sm btn-link text-body-secondary p-0" data-bs-toggle="dropdown">
<i class="fas fa-download"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% with export_security_compliance_url=product.get_export_security_compliance_url %}
<li>
<a class="dropdown-item" href="{{ export_security_compliance_url }}?export=csv">
<i class="fas fa-download me-1"></i>{% trans "Comma-separated Values (.csv)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_security_compliance_url }}?export=json">
<i class="fas fa-download me-1"></i>{% trans "JSON (.json)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_security_compliance_url }}?export=ods">
<i class="fas fa-download me-1"></i>{% trans "OpenDocument (.ods)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_security_compliance_url }}?export=xlsx">
<i class="fas fa-download me-1"></i>{% trans "Microsoft Excel (.xlsx)" %}
</a>
</li>
<li>
<a class="dropdown-item" href="{{ export_security_compliance_url }}?export=yaml">
<i class="fas fa-download me-1"></i>{% trans "YAML (.yaml)" %}
</a>
</li>
{% endwith %}
</ul>
</div>
{% if vulnerability_count == 0 or above_threshold_count == 0 %}
<span class="badge text-bg-success">{% trans "OK" %}</span>
{% elif max_vulnerability_severity == "critical" %}
<span class="badge text-bg-danger">{% trans "Critical" %}</span>
{% elif max_vulnerability_severity == "high" %}
<span class="badge bg-warning-orange">{% trans "High" %}</span>
{% elif max_vulnerability_severity == "medium" %}
<span class="badge text-bg-warning">{% trans "Medium" %}</span>
{% else %}
<span class="badge text-bg-info">{% trans "Low" %}</span>
{% endif %}
</div>
</div>

{# Summary #}
Expand Down
2 changes: 1 addition & 1 deletion product_portfolio/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -573,7 +573,7 @@ def test_api_product_endpoint_cyclonedx_sbom_action(self):

response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
expected = 'attachment; filename="dejacode_nexb_product_p1.cdx.json"'
expected = 'attachment; filename="dejacode_nexb_p1.cdx.json"'
self.assertEqual(expected, response["Content-Disposition"])
self.assertEqual("application/json", response["Content-Type"])
self.assertIn('"specVersion": "1.6"', str(response.getvalue()))
Expand Down
Loading
Loading