Skip to content

Commit

Permalink
Refactor the outputs views and add CycloneDX download in API #60
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez committed Apr 24, 2024
1 parent 1380858 commit 633ef6c
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 110 deletions.
67 changes: 67 additions & 0 deletions dje/outputs.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@

import re

from django.http import FileResponse
from django.http import Http404

from cyclonedx import output as cyclonedx_output
from cyclonedx.model import bom as cyclonedx_bom

from dejacode import __version__ as dejacode_version
from dejacode_toolkit import spdx

Expand All @@ -17,6 +23,20 @@ def safe_filename(filename):
return re.sub("[^A-Za-z0-9.-]+", "_", filename).lower()


def get_attachment_response(file_content, filename):
if not file_content or not filename:
raise Http404

response = FileResponse(
file_content,
filename=filename,
content_type="application/json",
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'

return response


def get_spdx_extracted_licenses(spdx_packages):
"""
Return all the licenses to be included in the SPDX extracted_licenses.
Expand Down Expand Up @@ -68,3 +88,50 @@ 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):
"""https://cyclonedx.org/use-cases/#dependency-graph"""
cyclonedx_components = []

if hasattr(instance, "get_cyclonedx_components"):
cyclonedx_components = [
component.as_cyclonedx() for component in instance.get_cyclonedx_components()
]

bom = cyclonedx_bom.Bom(components=cyclonedx_components)

cdx_component = instance.as_cyclonedx()
cdx_component.dependencies.update([component.bom_ref for component in cyclonedx_components])

bom.metadata = cyclonedx_bom.BomMetaData(
component=cdx_component,
tools=[
cyclonedx_bom.Tool(
vendor="nexB",
name="DejaCode",
version=dejacode_version,
)
],
authors=[
cyclonedx_bom.OrganizationalContact(
name=f"{user.first_name} {user.last_name}",
)
],
)

return bom


def get_cyclonedx_bom_json(cyclonedx_bom):
outputter = cyclonedx_output.get_instance(
bom=cyclonedx_bom,
output_format=cyclonedx_output.OutputFormat.JSON,
)
return outputter.output_as_string()


def get_cyclonedx_filename(instance):
base_filename = f"dejacode_{instance.dataspace.name}_{instance._meta.model_name}"
filename = f"{base_filename}_{instance}.cdx.json"
return safe_filename(filename)
50 changes: 50 additions & 0 deletions dje/tests/test_outputs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#
# Copyright (c) nexB Inc. and others. All rights reserved.
# DejaCode is a trademark of nexB Inc.
# SPDX-License-Identifier: AGPL-3.0-only
# See https://github.com/nexB/dejacode for support or download.
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#

from django.test import TestCase

from dejacode import __version__ as dejacode_version
from dje import outputs
from dje.models import Dataspace
from dje.tests import create_superuser
from dje.tests import create_user
from product_portfolio.models import Product


class OutputsTestCase(TestCase):
def setUp(self):
self.dataspace = Dataspace.objects.create(name="nexB")
self.super_user = create_superuser("nexb_user", self.dataspace)
self.basic_user = create_user("basic_user", self.dataspace)

self.product1 = Product.objects.create(
name="Product1 With Space", version="1.0", dataspace=self.dataspace
)

def test_outputs_get_spdx_document(self):
document = outputs.get_spdx_document(self.product1, self.super_user)
document.creation_info.created = "2000-01-01T01:02:03Z"
expected = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "dejacode_nexb_product_product1_with_space_1.0",
"documentNamespace": f"https://dejacode.com/spdxdocs/{self.product1.uuid}",
"creationInfo": {
"created": "2000-01-01T01:02:03Z",
"creators": [
"Person: (user@email.com)",
"Organization: nexB ()",
f"Tool: DejaCode-{dejacode_version}",
],
"licenseListVersion": "3.18",
},
"packages": [],
"documentDescribes": [],
}
self.assertEqual(expected, document.as_dict())
76 changes: 9 additions & 67 deletions dje/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,15 +67,12 @@
from django.views.generic.edit import DeleteView

import django_otp
from cyclonedx import output as cyclonedx_output
from cyclonedx.model import bom as cyclonedx_bom
from django_filters.views import FilterView
from grappelli.views.related import AutocompleteLookup
from grappelli.views.related import RelatedLookup
from notifications import views as notifications_views

from component_catalog.license_expression_dje import get_license_objects
from dejacode import __version__ as dejacode_version
from dejacode_toolkit.purldb import PurlDB
from dejacode_toolkit.scancodeio import ScanCodeIO
from dejacode_toolkit.vulnerablecode import VulnerableCode
Expand Down Expand Up @@ -118,7 +115,6 @@
from dje.utils import group_by_simple
from dje.utils import has_permission
from dje.utils import queryset_to_changelist_href
from dje.utils import safe_filename
from dje.utils import str_to_id_list

License = apps.get_model("license_library", "License")
Expand Down Expand Up @@ -2324,19 +2320,12 @@ class ExportSPDXDocumentView(
):
def get(self, request, *args, **kwargs):
spdx_document = outputs.get_spdx_document(self.get_object(), self.request.user)
spdx_document_json = spdx_document.as_json()

if not spdx_document:
raise Http404

filename = outputs.get_spdx_filename(spdx_document)
response = FileResponse(
spdx_document.as_json(),
filename=filename,
content_type="application/json",
return outputs.get_attachment_response(
file_content=spdx_document_json,
filename=outputs.get_spdx_filename(spdx_document),
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'

return response


class ExportCycloneDXBOMView(
Expand All @@ -2347,57 +2336,10 @@ class ExportCycloneDXBOMView(
):
def get(self, request, *args, **kwargs):
instance = self.get_object()
cyclonedx_bom = self.get_cyclonedx_bom(instance, self.request.user)
base_filename = f"dejacode_{instance.dataspace.name}_{instance._meta.model_name}"
filename = safe_filename(f"{base_filename}_{instance}.cdx.json")

if not cyclonedx_bom:
raise Http404
cyclonedx_bom = outputs.get_cyclonedx_bom(instance, self.request.user)
cyclonedx_bom_json = outputs.get_cyclonedx_bom_json(cyclonedx_bom)

outputter = cyclonedx_output.get_instance(
bom=cyclonedx_bom,
output_format=cyclonedx_output.OutputFormat.JSON,
return outputs.get_attachment_response(
file_content=cyclonedx_bom_json,
filename=outputs.get_cyclonedx_filename(instance),
)
bom_json = outputter.output_as_string()

response = FileResponse(
bom_json,
filename=filename,
content_type="application/json",
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'

return response

@staticmethod
def get_cyclonedx_bom(instance, user):
"""https://cyclonedx.org/use-cases/#dependency-graph"""
cyclonedx_components = []

if hasattr(instance, "get_cyclonedx_components"):
cyclonedx_components = [
component.as_cyclonedx() for component in instance.get_cyclonedx_components()
]

bom = cyclonedx_bom.Bom(components=cyclonedx_components)

cdx_component = instance.as_cyclonedx()
cdx_component.dependencies.update([component.bom_ref for component in cyclonedx_components])

bom.metadata = cyclonedx_bom.BomMetaData(
component=cdx_component,
tools=[
cyclonedx_bom.Tool(
vendor="nexB",
name="DejaCode",
version=dejacode_version,
)
],
authors=[
cyclonedx_bom.OrganizationalContact(
name=f"{user.first_name} {user.last_name}",
)
],
)

return bom
35 changes: 17 additions & 18 deletions product_portfolio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
#

from django.core.exceptions import ValidationError
from django.http import FileResponse
from django.http import Http404

import django_filters
from rest_framework import permissions
Expand Down Expand Up @@ -404,32 +402,33 @@ def pull_scancodeio_project_data(self, request, *args, **kwargs):

return Response({"status": "Packages import from ScanCode.io in progress..."})

@action(detail=True)
def about_files(self, request, uuid):
@action(detail=True, name="Download AboutCode files")
def aboutcode_files(self, request, uuid):
instance = self.get_object()
about_files = instance.get_about_files()
filename = self.get_filename(instance)
return self.get_zipped_response(about_files, filename)

@action(detail=True)
@action(detail=True, name="Download SPDX document")
def spdx_document(self, request, uuid):
spdx_document = outputs.get_spdx_document(
instance=self.get_object(),
user=self.request.user,
spdx_document = outputs.get_spdx_document(self.get_object(), self.request.user)
spdx_document_json = spdx_document.as_json()

return outputs.get_attachment_response(
file_content=spdx_document_json,
filename=outputs.get_spdx_filename(spdx_document),
)

if not spdx_document:
raise Http404
@action(detail=True, name="Download CycloneDX SBOM")
def cyclonedx_sbom(self, request, uuid):
instance = self.get_object()
cyclonedx_bom = outputs.get_cyclonedx_bom(instance, self.request.user)
cyclonedx_bom_json = outputs.get_cyclonedx_bom_json(cyclonedx_bom)

filename = outputs.get_spdx_filename(spdx_document)
response = FileResponse(
spdx_document.as_json(),
filename=filename,
content_type="application/json",
return outputs.get_attachment_response(
file_content=cyclonedx_bom_json,
filename=outputs.get_cyclonedx_filename(instance),
)
response["Content-Disposition"] = f'attachment; filename="{filename}"'

return response


class BaseProductRelationSerializer(ValidateLicenseExpressionMixin, DataspacedSerializer):
Expand Down
26 changes: 1 addition & 25 deletions product_portfolio/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,16 @@
from component_catalog.models import ComponentAssignedPackage
from component_catalog.models import ComponentKeyword
from component_catalog.models import Package
from dejacode import __version__ as dejacode_version
from dejacode_toolkit import scancodeio
from dje.models import Dataspace
from dje.models import History
from dje.outputs import get_spdx_extracted_licenses
from dje.tasks import logger as tasks_logger
from dje.tasks import pull_project_data_from_scancodeio
from dje.tasks import scancodeio_submit_project
from dje.tests import add_perms
from dje.tests import create_superuser
from dje.tests import create_user
from dje.views import ExportSPDXDocumentView
from dje.views import get_spdx_extracted_licenses
from license_library.models import License
from organization.models import Owner
from policy.models import UsagePolicy
Expand Down Expand Up @@ -2460,28 +2458,6 @@ def test_product_portfolio_product_export_spdx_view(self):
)
self.assertEqual("application/json", response.headers["Content-Type"])

document = ExportSPDXDocumentView.get_spdx_document(self.product1, self.super_user)
document.creation_info.created = "2000-01-01T01:02:03Z"
expected = {
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "dejacode_nexb_product_product1_with_space_1.0",
"documentNamespace": f"https://dejacode.com/spdxdocs/{self.product1.uuid}",
"creationInfo": {
"created": "2000-01-01T01:02:03Z",
"creators": [
"Person: (user@email.com)",
"Organization: nexB ()",
f"Tool: DejaCode-{dejacode_version}",
],
"licenseListVersion": "3.18",
},
"packages": [],
"documentDescribes": [],
}
self.assertEqual(expected, document.as_dict())

def test_product_portfolio_product_export_spdx_get_spdx_extracted_licenses(self):
owner1 = Owner.objects.create(name="Owner1", dataspace=self.dataspace)
license1 = License.objects.create(
Expand Down

0 comments on commit 633ef6c

Please sign in to comment.