diff --git a/CHANGELOG.rst b/CHANGELOG.rst
index 6e28c6ab..c0dfefcf 100644
--- a/CHANGELOG.rst
+++ b/CHANGELOG.rst
@@ -31,6 +31,9 @@ Release notes
- Refactor the "Import manifest" feature as "Load SBOMs".
https://github.com/nexB/dejacode/issues/61
+- Add support to import packages from manifest.
+ https://github.com/nexB/dejacode/issues/65
+
- Add a vulnerability link to the VulnerableCode app in the Vulnerability tab.
https://github.com/nexB/dejacode/issues/4
diff --git a/dejacode_toolkit/scancodeio.py b/dejacode_toolkit/scancodeio.py
index 962043e5..332217a8 100644
--- a/dejacode_toolkit/scancodeio.py
+++ b/dejacode_toolkit/scancodeio.py
@@ -76,10 +76,12 @@ def submit_scan(self, uri, user_uuid, dataspace_uuid):
logger.debug(f'{self.label}: submit scan uri="{uri}" webhook_url="{webhook_url}"')
return self.request_post(url=self.project_api_url, json=data)
- def submit_load_sbom(self, project_name, file_location, user_uuid, execute_now=False):
+ def submit_project(
+ self, project_name, pipeline_name, file_location, user_uuid, execute_now=False
+ ):
data = {
"name": project_name,
- "pipeline": "load_sbom",
+ "pipeline": pipeline_name,
"execute_now": execute_now,
}
files = {
@@ -92,7 +94,7 @@ def submit_load_sbom(self, project_name, file_location, user_uuid, execute_now=F
data["webhook_url"] = webhook_url
logger.debug(
- f"{self.label}: submit load sbom "
+ f"{self.label}: submit pipeline={pipeline_name} "
f'project_name="{project_name}" webhook_url="{webhook_url}"'
)
return self.request_post(url=self.project_api_url, data=data, files=files)
diff --git a/dje/tasks.py b/dje/tasks.py
index 3f7c810d..724fd458 100644
--- a/dje/tasks.py
+++ b/dje/tasks.py
@@ -115,13 +115,14 @@ def scancodeio_submit_scan(uris, user_uuid, dataspace_uuid):
@job
-def scancodeio_submit_load_sbom(scancodeproject_uuid, user_uuid):
+def scancodeio_submit_project(scancodeproject_uuid, user_uuid, pipeline_name):
"""Submit the provided SBOM file to ScanCode.io as an asynchronous task."""
from dje.models import DejacodeUser
logger.info(
- f"Entering scancodeio_submit_load_sbom task with "
- f"scancodeproject_uuid={scancodeproject_uuid} user_uuid={user_uuid}"
+ f"Entering scancodeio_submit_project task with "
+ f"scancodeproject_uuid={scancodeproject_uuid} user_uuid={user_uuid} "
+ f"pipeline_name={pipeline_name}"
)
ScanCodeProject = apps.get_model("product_portfolio", "scancodeproject")
@@ -137,24 +138,25 @@ def scancodeio_submit_load_sbom(scancodeproject_uuid, user_uuid):
# Create a Project instance on ScanCode.io without immediate execution of the
# pipeline. This allows to get instant feedback from ScanCode.io about the Project
# creation status and its related data, even in SYNC mode.
- response = scancodeio.submit_load_sbom(
+ response = scancodeio.submit_project(
project_name=scancodeproject_uuid,
+ pipeline_name=pipeline_name,
file_location=scancode_project.input_file.path,
user_uuid=user_uuid,
execute_now=False,
)
if not response:
- logger.info("Error submitting the SBOM file to ScanCode.io server")
+ logger.info("Error submitting the file to ScanCode.io server")
scancode_project.status = ScanCodeProject.Status.FAILURE
- msg = "- Error: SBOM could not be submitted to ScanCode.io"
+ msg = "- Error: File could not be submitted to ScanCode.io"
scancode_project.append_to_log(msg, save=True)
return
logger.info("Update the ScanCodeProject instance")
scancode_project.status = ScanCodeProject.Status.SUBMITTED
scancode_project.project_uuid = response.get("uuid")
- msg = "- SBOM file submitted to ScanCode.io for inspection"
+ msg = "- File submitted to ScanCode.io for inspection"
scancode_project.append_to_log(msg, save=True)
# Delay the execution of the pipeline after the ScancodeProject instance was
@@ -164,7 +166,7 @@ def scancodeio_submit_load_sbom(scancodeproject_uuid, user_uuid):
transaction.on_commit(lambda: scancodeio.start_pipeline(run_url=runs[0]["url"]))
-@job
+@job("default", timeout=1200)
def pull_project_data_from_scancodeio(scancodeproject_uuid):
"""
Pull Project data from ScanCode.io as an asynchronous task for the provided
diff --git a/product_portfolio/api.py b/product_portfolio/api.py
index c8c4a1d8..45a22304 100644
--- a/product_portfolio/api.py
+++ b/product_portfolio/api.py
@@ -35,6 +35,7 @@
from dje.permissions import assign_all_object_permissions
from product_portfolio.filters import ComponentCompletenessAPIFilter
from product_portfolio.forms import ImportFromScanForm
+from product_portfolio.forms import ImportManifestsForm
from product_portfolio.forms import LoadSBOMsForm
from product_portfolio.forms import PullProjectDataForm
from product_portfolio.models import CodebaseResource
@@ -216,6 +217,25 @@ class LoadSBOMsFormSerializer(serializers.Serializer):
)
+class ImportManifestsFormSerializer(serializers.Serializer):
+ """Serializer equivalent of ImportManifestsForm, used for API documentation."""
+
+ input_file = serializers.FileField(
+ required=True,
+ help_text=ImportManifestsForm.base_fields["input_file"].label,
+ )
+ update_existing_packages = serializers.BooleanField(
+ required=False,
+ default=False,
+ help_text=ImportManifestsForm.base_fields["update_existing_packages"].help_text,
+ )
+ scan_all_packages = serializers.BooleanField(
+ required=False,
+ default=False,
+ help_text=ImportManifestsForm.base_fields["scan_all_packages"].help_text,
+ )
+
+
class ImportFromScanSerializer(serializers.Serializer):
"""Serializer equivalent of ImportFromScanForm, used for API documentation."""
@@ -319,6 +339,23 @@ def load_sboms(self, request, *args, **kwargs):
form.submit(product=product, user=request.user)
return Response({"status": "SBOM file submitted to ScanCode.io for inspection."})
+ @action(detail=True, methods=["post"], serializer_class=ImportManifestsFormSerializer)
+ def import_manifests(self, request, *args, **kwargs):
+ """
+ Import Packages from Manifests.
+
+ Multiple Manifests: You can provide multiple files by packaging them into a zip
+ archive. DejaCode will handle and process them accordingly.
+ """
+ product = self.get_object()
+
+ form = ImportManifestsForm(data=request.POST, files=request.FILES)
+ if not form.is_valid():
+ return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
+
+ form.submit(product=product, user=request.user)
+ return Response({"status": "Manifest file submitted to ScanCode.io for inspection."})
+
@action(detail=True, methods=["post"], serializer_class=ImportFromScanSerializer)
def import_from_scan(self, request, *args, **kwargs):
"""
diff --git a/product_portfolio/forms.py b/product_portfolio/forms.py
index 2b901568..29d3681a 100644
--- a/product_portfolio/forms.py
+++ b/product_portfolio/forms.py
@@ -587,11 +587,15 @@ def save(self, product):
return warnings, created_counts
-class LoadSBOMsForm(forms.Form):
+class BaseProductImportFormView(forms.Form):
+ project_type = None
+ input_label = ""
+
input_file = SmartFileField(
- label=_("SBOM file or zip archive"),
+ label=_("file or zip archive"),
required=True,
)
+
update_existing_packages = forms.BooleanField(
label=_("Update existing packages with discovered packages data"),
required=False,
@@ -614,6 +618,10 @@ class LoadSBOMsForm(forms.Form):
),
)
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["input_file"].label = _(f"{self.input_label} file or zip archive")
+
@property
def helper(self):
helper = FormHelper()
@@ -627,7 +635,7 @@ def submit(self, product, user):
scancode_project = ScanCodeProject.objects.create(
product=product,
dataspace=product.dataspace,
- type=ScanCodeProject.ProjectType.LOAD_SBOMS,
+ type=self.project_type,
input_file=self.cleaned_data.get("input_file"),
update_existing_packages=self.cleaned_data.get("update_existing_packages"),
scan_all_packages=self.cleaned_data.get("scan_all_packages"),
@@ -635,13 +643,26 @@ def submit(self, product, user):
)
transaction.on_commit(
- lambda: tasks.scancodeio_submit_load_sbom.delay(
+ lambda: tasks.scancodeio_submit_project.delay(
scancodeproject_uuid=scancode_project.uuid,
user_uuid=user.uuid,
+ pipeline_name=self.pipeline_name,
)
)
+class LoadSBOMsForm(BaseProductImportFormView):
+ project_type = ScanCodeProject.ProjectType.LOAD_SBOMS
+ input_label = "SBOM"
+ pipeline_name = "load_sbom"
+
+
+class ImportManifestsForm(BaseProductImportFormView):
+ project_type = ScanCodeProject.ProjectType.IMPORT_FROM_MANIFEST
+ input_label = "Manifest"
+ pipeline_name = "resolve_dependencies"
+
+
class StrongTextWidget(forms.Widget):
def render(self, name, value, attrs=None, renderer=None):
if value:
diff --git a/product_portfolio/importers.py b/product_portfolio/importers.py
index bbc8cde1..d831f1bc 100644
--- a/product_portfolio/importers.py
+++ b/product_portfolio/importers.py
@@ -13,6 +13,7 @@
from django.core.exceptions import MultipleObjectsReturned
from django.core.exceptions import ValidationError
from django.core.validators import EMPTY_VALUES
+from django.db import IntegrityError
from django.db import transaction
from django.db.models import ObjectDoesNotExist
from django.db.models import Q
@@ -26,11 +27,13 @@
from component_catalog.models import Component
from component_catalog.models import Package
from dejacode_toolkit.scancodeio import ScanCodeIO
+from dje.copier import copy_object
from dje.importers import BaseImporter
from dje.importers import BaseImportModelForm
from dje.importers import BaseImportModelFormSet
from dje.importers import ComponentRelatedFieldImportMixin
from dje.importers import ModelChoiceFieldForImport
+from dje.models import Dataspace
from dje.utils import get_help_text
from dje.utils import is_uuid4
from product_portfolio.forms import ProductComponentLicenseExpressionFormMixin
@@ -667,12 +670,26 @@ def import_package(self, package_data):
if (value := package_data.get(field))
}
+ # Check if the Package already exists in the local Dataspace
try:
package = Package.objects.scope(self.user.dataspace).get(**unique_together_lookups)
self.existing.append(package)
except (ObjectDoesNotExist, MultipleObjectsReturned):
package = None
+ # Check if the Package already exists in the reference Dataspace
+ reference_dataspace = Dataspace.objects.get_reference()
+ user_dataspace = self.user.dataspace
+ if not package and user_dataspace != reference_dataspace:
+ qs = Package.objects.scope(reference_dataspace).filter(**unique_together_lookups)
+ if qs.exists():
+ reference_object = qs.first()
+ try:
+ package = copy_object(reference_object, user_dataspace, self.user, update=False)
+ self.created.append(package)
+ except IntegrityError as error:
+ self.errors.append(error)
+
if license_expression := package_data.get("declared_license_expression"):
license_expression = str(self.licensing.dedup(license_expression))
package_data["license_expression"] = license_expression
@@ -683,8 +700,8 @@ def import_package(self, package_data):
if not package:
try:
package = Package.create_from_data(self.user, package_data, validate=True)
- except ValidationError as e:
- self.errors.append(e)
+ except ValidationError as errors:
+ self.errors.append(errors)
return
self.created.append(package)
diff --git a/product_portfolio/models.py b/product_portfolio/models.py
index baa72ac2..61f6573f 100644
--- a/product_portfolio/models.py
+++ b/product_portfolio/models.py
@@ -254,6 +254,9 @@ def get_check_package_version_url(self):
def get_load_sboms_url(self):
return self.get_url("load_sboms")
+ def get_import_manifests_url(self):
+ return self.get_url("import_manifests")
+
def get_pull_project_data_url(self):
return self.get_url("pull_project_data")
@@ -1119,7 +1122,6 @@ class ScanCodeProject(HistoryFieldsMixin, DataspacedModel):
"""Wrap a ScanCode.io Project."""
class ProjectType(models.TextChoices):
- # This type was replaced by LOAD_SBOMS but is kept for backward compatibility
IMPORT_FROM_MANIFEST = "IMPORT_FROM_MANIFEST", _("Import from Manifest")
LOAD_SBOMS = "LOAD_SBOMS", _("Load SBOMs")
PULL_FROM_SCANCODEIO = "PULL_FROM_SCANCODEIO", _("Pull from ScanCode.io")
diff --git a/product_portfolio/templates/product_portfolio/import_manifests_form.html b/product_portfolio/templates/product_portfolio/import_manifests_form.html
new file mode 100644
index 00000000..f806f77a
--- /dev/null
+++ b/product_portfolio/templates/product_portfolio/import_manifests_form.html
@@ -0,0 +1,77 @@
+{% extends "bootstrap_base.html" %}
+{% load i18n static crispy_forms_tags %}
+{% load inject_preserved_filters from dje_tags %}
+
+{% block page_title %}{% trans "Import Packages from manifests" %}{% endblock %}
+
+{% block content %}
+
+
+ {% include 'includes/messages_alert.html' %}
+
+
+
+ Supports resolving packages for:
+
+ - Python: requirements.txt and setup.py manifest files.
+
+
+
Multiple Manifests:
+ You can provide multiple Manifests by packaging them into a
zip archive.
+ DejaCode will handle and process them accordingly.
+
+
+
+ When you upload your
Manifest file to DejaCode,
+ the following process will occur:
+
+ -
+ Submission to ScanCode.io
+ Your Manifest file will be submitted to ScanCode.io for thorough scan inspection.
+
+ -
+ Package Discovery
+ ScanCode.io will identify and discover packages within your Manifest.
+
+ -
+ Package Importation
+ DejaCode will retrieve the discovered packages from ScanCode.io and import them into its system.
+
+ -
+ Package Assignment
+ The imported packages will be assigned to the corresponding product within DejaCode.
+
+
+
+
+
+
+ {{ form.errors }}
+ {% crispy form %}
+
+
+{% endblock %}
+
+{% block javascripts %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/product_portfolio/templates/product_portfolio/product_details.html b/product_portfolio/templates/product_portfolio/product_details.html
index c68411d0..da4c0ae4 100644
--- a/product_portfolio/templates/product_portfolio/product_details.html
+++ b/product_portfolio/templates/product_portfolio/product_details.html
@@ -39,6 +39,7 @@
{% trans 'Import data from Scan' %}
{% if request.user.dataspace.enable_package_scanning %}
{% trans 'Load Packages from SBOMs' %}
+ {% trans 'Import Packages from manifests' %}
{% endif %}
{% if pull_project_data_form %}
{% trans 'Pull ScanCode.io Project data' %}
diff --git a/product_portfolio/templates/product_portfolio/scancodeio_project_status.html b/product_portfolio/templates/product_portfolio/scancodeio_project_status.html
index 822d746b..e3e03309 100644
--- a/product_portfolio/templates/product_portfolio/scancodeio_project_status.html
+++ b/product_portfolio/templates/product_portfolio/scancodeio_project_status.html
@@ -1,6 +1,37 @@
+{% load humanize %}
+
+{% if scancode_project.results.errors %}
+ {{ scancode_project.results.errors|length }} errors:
+
+ {% for error in scancode_project.results.errors %}
+ - {{ error }}
+ {% endfor %}
+
+
+{% endif %}
+{% if scancode_project.results.created %}
+ {{ scancode_project.results.created|length }} packages created:
+
+ {% for package in scancode_project.results.created %}
+ - {{ package }}
+ {% endfor %}
+
+
+{% endif %}
+{% if scancode_project.results.existing %}
+ {{ scancode_project.results.existing|length }} packages updated/existing:
+
+ {% for package in scancode_project.results.existing %}
+ - {{ package }}
+ {% endfor %}
+
+
+{% endif %}
+
- - Package count: {{ scan_data.package_count }}
- - Resource count: {{ scan_data.resource_count }}
+ - Package count: {{ scan_data.package_count|intcomma }}
+ - Dependency count: {{ scan_data.dependency_count|intcomma }}
+ - Resource count: {{ scan_data.resource_count|intcomma }}
{% for run in scan_data.runs %}
diff --git a/product_portfolio/templates/product_portfolio/scancodeio_pull_data_status.html b/product_portfolio/templates/product_portfolio/scancodeio_pull_data_status.html
deleted file mode 100644
index cb4cd092..00000000
--- a/product_portfolio/templates/product_portfolio/scancodeio_pull_data_status.html
+++ /dev/null
@@ -1,28 +0,0 @@
-{% if scancode_project.results.errors %}
- {{ scancode_project.results.errors|length }} errors:
-
- {% for error in scancode_project.results.errors %}
- - {{ error }}
- {% endfor %}
-
-
-{% endif %}
-
-{% if scancode_project.results.created %}
- {{ scancode_project.results.created|length }} packages created:
-
- {% for package in scancode_project.results.created %}
- - {{ package }}
- {% endfor %}
-
-
-{% endif %}
-
-{% if scancode_project.results.existing %}
- {{ scancode_project.results.existing|length }} packages updated/existing:
-
- {% for package in scancode_project.results.existing %}
- - {{ package }}
- {% endfor %}
-
-{% endif %}
\ No newline at end of file
diff --git a/product_portfolio/tests/test_api.py b/product_portfolio/tests/test_api.py
index 9001aabf..9c6bd1c3 100644
--- a/product_portfolio/tests/test_api.py
+++ b/product_portfolio/tests/test_api.py
@@ -379,6 +379,35 @@ def test_api_product_endpoint_load_sboms_action(self):
self.assertEqual(expected, response.data)
self.assertEqual(1, ScanCodeProject.objects.count())
+ def test_api_product_endpoint_import_manifests_action(self):
+ url = reverse("api_v2:product-import-manifests", args=[self.product1.uuid])
+
+ self.client.login(username=self.base_user.username, password="secret")
+ response = self.client.get(url)
+ self.assertEqual(status.HTTP_405_METHOD_NOT_ALLOWED, response.status_code)
+ response = self.client.post(url, data={})
+ self.assertEqual(status.HTTP_403_FORBIDDEN, response.status_code)
+
+ # Required permissions
+ add_perm(self.base_user, "add_product")
+ assign_perm("view_product", self.base_user, self.product1)
+
+ response = self.client.post(url, data={})
+ self.assertEqual(status.HTTP_400_BAD_REQUEST, response.status_code)
+ expected = {"input_file": ["This field is required."]}
+ self.assertEqual(expected, response.data)
+
+ data = {
+ "input_file": ContentFile("Content", name="requirements.txt"),
+ "update_existing_packages": False,
+ "scan_all_packages": False,
+ }
+ response = self.client.post(url, data)
+ self.assertEqual(status.HTTP_200_OK, response.status_code)
+ expected = {"status": "Manifest file submitted to ScanCode.io for inspection."}
+ self.assertEqual(expected, response.data)
+ self.assertEqual(1, ScanCodeProject.objects.count())
+
def test_api_product_endpoint_import_from_scan_action(self):
url = reverse("api_v2:product-import-from-scan", args=[self.product1.uuid])
diff --git a/product_portfolio/tests/test_views.py b/product_portfolio/tests/test_views.py
index 9ca75abc..823bbf94 100644
--- a/product_portfolio/tests/test_views.py
+++ b/product_portfolio/tests/test_views.py
@@ -34,7 +34,7 @@
from dje.models import History
from dje.tasks import logger as tasks_logger
from dje.tasks import pull_project_data_from_scancodeio
-from dje.tasks import scancodeio_submit_load_sbom
+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
@@ -2693,8 +2693,8 @@ def test_product_portfolio_product_export_cyclonedx_view(self):
self.assertEqual(expected, content)
- @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_load_sbom")
- def test_scancodeio_submit_load_sbom_task(self, mock_submit_sbom):
+ @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project")
+ def test_scancodeio_submit_project_task(self, mock_submit_project):
scancodeproject = ScanCodeProject.objects.create(
product=self.product1,
dataspace=self.product1.dataspace,
@@ -2702,15 +2702,16 @@ def test_scancodeio_submit_load_sbom_task(self, mock_submit_sbom):
input_file=ContentFile("Data", name="data.json"),
)
- mock_submit_sbom.return_value = None
- scancodeio_submit_load_sbom(
+ mock_submit_project.return_value = None
+ scancodeio_submit_project(
scancodeproject_uuid=scancodeproject.uuid,
user_uuid=self.super_user.uuid,
+ pipeline_name="load_sboms",
)
scancodeproject.refresh_from_db()
self.assertEqual("failure", scancodeproject.status)
self.assertIsNone(scancodeproject.project_uuid)
- expected = ["- Error: SBOM could not be submitted to ScanCode.io"]
+ expected = ["- Error: File could not be submitted to ScanCode.io"]
self.assertEqual(expected, scancodeproject.import_log)
# Reset the instance values
@@ -2719,19 +2720,20 @@ def test_scancodeio_submit_load_sbom_task(self, mock_submit_sbom):
scancodeproject.save()
project_uuid = uuid.uuid4()
- mock_submit_sbom.return_value = {"uuid": project_uuid}
- scancodeio_submit_load_sbom(
+ mock_submit_project.return_value = {"uuid": project_uuid}
+ scancodeio_submit_project(
scancodeproject_uuid=scancodeproject.uuid,
user_uuid=self.super_user.uuid,
+ pipeline_name="load_sboms",
)
scancodeproject.refresh_from_db()
self.assertEqual("submitted", scancodeproject.status)
self.assertEqual(project_uuid, scancodeproject.project_uuid)
- expected = ["- SBOM file submitted to ScanCode.io for inspection"]
+ expected = ["- File submitted to ScanCode.io for inspection"]
self.assertEqual(expected, scancodeproject.import_log)
- @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_load_sbom")
+ @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project")
def test_product_portfolio_load_sbom_view(self, mock_submit):
mock_submit.return_value = None
self.client.login(username=self.super_user.username, password="secret")
@@ -2752,6 +2754,27 @@ def test_product_portfolio_load_sbom_view(self, mock_submit):
self.client.post(url, data=data, follow=True)
scan.assert_called_once()
+ @mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.submit_project")
+ def test_product_portfolio_mport_manifest_view(self, mock_submit):
+ mock_submit.return_value = None
+ self.client.login(username=self.super_user.username, password="secret")
+ url = self.product1.get_import_manifests_url()
+ response = self.client.get(url)
+ expected = "Import Packages from manifests"
+ self.assertContains(response, expected)
+
+ data = {"input_file": ContentFile("Data")}
+ response = self.client.post(url, data=data, follow=True)
+ expected = "Manifest file submitted to ScanCode.io for inspection."
+ self.assertContains(response, expected)
+ self.assertEqual(1, ScanCodeProject.objects.count())
+
+ with override_settings(CLAMD_ENABLED=True):
+ with mock.patch("dje.fields.SmartFileField.scan_file_for_virus") as scan:
+ data = {"input_file": ContentFile("Data")}
+ self.client.post(url, data=data, follow=True)
+ scan.assert_called_once()
+
@mock.patch("dejacode_toolkit.scancodeio.ScanCodeIO.fetch_project_packages")
def test_product_portfolio_import_packages_from_scancodeio_view(self, mock_fetch_packages):
self.client.login(username=self.super_user.username, password="secret")
diff --git a/product_portfolio/urls.py b/product_portfolio/urls.py
index e10f81ac..c63227c9 100644
--- a/product_portfolio/urls.py
+++ b/product_portfolio/urls.py
@@ -9,6 +9,7 @@
from django.urls import path
from product_portfolio.views import AttributionView
+from product_portfolio.views import ImportManifestsView
from product_portfolio.views import LoadSBOMsView
from product_portfolio.views import ManageComponentGridView
from product_portfolio.views import ManagePackageGridView
@@ -88,6 +89,7 @@ def product_path(path_segment, view):
*product_path("license_summary", license_summary_view),
*product_path("check_package_version", check_package_version_ajax_view),
*product_path("load_sboms", LoadSBOMsView.as_view()),
+ *product_path("import_manifests", ImportManifestsView.as_view()),
*product_path("tab_codebase", ProductTabCodebaseView.as_view()),
*product_path("tab_imports", ProductTabImportsView.as_view()),
*product_path("pull_project_data", PullProjectDataFromScanCodeIOView.as_view()),
diff --git a/product_portfolio/views.py b/product_portfolio/views.py
index b15a68a9..2cd95eaf 100644
--- a/product_portfolio/views.py
+++ b/product_portfolio/views.py
@@ -99,6 +99,7 @@
from product_portfolio.forms import BaseProductRelationshipInlineFormSet
from product_portfolio.forms import ComparisonExcludeFieldsForm
from product_portfolio.forms import ImportFromScanForm
+from product_portfolio.forms import ImportManifestsForm
from product_portfolio.forms import LoadSBOMsForm
from product_portfolio.forms import ProductComponentForm
from product_portfolio.forms import ProductComponentInlineForm
@@ -780,7 +781,7 @@ class ProductTabImportsView(
def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
scancode_projects = self.object.scancodeprojects.all()
- submitted_projects = self.get_submitted_load_sboms_projects(scancode_projects)
+ submitted_projects = self.get_submitted_projects(scancode_projects)
# Check the status of the "submitted" projects on ScanCode.io and update the
# local ScanCodeProject instances accordingly.
@@ -799,12 +800,16 @@ def get_context_data(self, **kwargs):
return context_data
@staticmethod
- def get_submitted_load_sboms_projects(scancode_projects):
+ def get_submitted_projects(scancode_projects):
+ submitted_types = [
+ ScanCodeProject.ProjectType.LOAD_SBOMS,
+ ScanCodeProject.ProjectType.IMPORT_FROM_MANIFEST,
+ ]
return [
project
for project in scancode_projects
if project.status == ScanCodeProject.Status.SUBMITTED
- and project.type == ScanCodeProject.ProjectType.LOAD_SBOMS
+ and project.type in submitted_types
]
def synchronize(self, scancodeio, project):
@@ -1834,7 +1839,7 @@ def get_latest_version_entry(current_uuid):
return JsonResponse({"success": "success", "upgrade_available": upgrade_available})
-class LoadSBOMsView(
+class BaseProductImportFormView(
LoginRequiredMixin,
PermissionRequiredMixin,
GetDataspacedObjectMixin,
@@ -1842,9 +1847,8 @@ class LoadSBOMsView(
BaseProductView,
FormView,
):
- template_name = "product_portfolio/load_sboms_form.html"
permission_required = "product_portfolio.change_product"
- form_class = LoadSBOMsForm
+ success_msg = ""
def get_queryset(self):
return self.model.objects.get_queryset(
@@ -1870,12 +1874,40 @@ def get_success_url(self):
def form_valid(self, form):
self.object = self.get_object()
- form.submit(product=self.object, user=self.request.user)
- msg = "SBOM file submitted to ScanCode.io for inspection."
- messages.success(self.request, msg)
+
+ try:
+ form.submit(product=self.object, user=self.request.user)
+ except ValidationError as error:
+ messages.error(self.request, error)
+ return redirect(self.object.get_absolute_url())
+
+ if self.success_msg:
+ messages.success(self.request, self.success_msg)
+
return super().form_valid(form)
+class LoadSBOMsView(BaseProductImportFormView):
+ template_name = "product_portfolio/load_sboms_form.html"
+ form_class = LoadSBOMsForm
+ success_msg = "SBOM file submitted to ScanCode.io for inspection."
+
+
+class ImportManifestsView(BaseProductImportFormView):
+ template_name = "product_portfolio/import_manifests_form.html"
+ form_class = ImportManifestsForm
+ success_msg = "Manifest file submitted to ScanCode.io for inspection."
+
+
+@method_decorator(require_POST, name="dispatch")
+class PullProjectDataFromScanCodeIOView(BaseProductImportFormView):
+ form_class = PullProjectDataForm
+ success_msg = "Packages import from ScanCode.io in progress..."
+
+ def form_invalid(self, form):
+ raise Http404
+
+
@require_POST
@csrf_exempt
def import_packages_from_scancodeio_view(request, key):
@@ -1911,65 +1943,18 @@ def import_packages_from_scancodeio_view(request, key):
@login_required
def scancodeio_project_status_view(request, scancodeproject_uuid):
+ template = "product_portfolio/scancodeio_project_status.html"
user = request.user
base_qs = ScanCodeProject.objects.scope(user.dataspace)
scancode_project = get_object_or_404(base_qs, uuid=scancodeproject_uuid)
- project_types = ScanCodeProject.ProjectType
scancodeio = ScanCodeIO(user)
- if scancode_project.type in [project_types.LOAD_SBOMS, project_types.IMPORT_FROM_MANIFEST]:
- template = "product_portfolio/scancodeio_project_status.html"
- scan_detail_url = scancodeio.get_scan_detail_url(scancode_project.project_uuid)
- scan_data = scancodeio.fetch_scan_data(scan_detail_url)
- context = {"scan_data": scan_data}
+ scan_detail_url = scancodeio.get_scan_detail_url(scancode_project.project_uuid)
+ scan_data = scancodeio.fetch_scan_data(scan_detail_url)
- elif scancode_project.type == project_types.PULL_FROM_SCANCODEIO:
- template = "product_portfolio/scancodeio_pull_data_status.html"
- context = {"scancode_project": scancode_project}
-
- else:
- raise Http404
+ context = {
+ "scancode_project": scancode_project,
+ "scan_data": scan_data,
+ }
return TemplateResponse(request, template, context)
-
-
-@method_decorator(require_POST, name="dispatch")
-class PullProjectDataFromScanCodeIOView(
- LoginRequiredMixin,
- PermissionRequiredMixin,
- GetDataspacedObjectMixin,
- DataspacedModelFormMixin,
- BaseProductView,
- FormView,
-):
- permission_required = "product_portfolio.change_product"
- form_class = PullProjectDataForm
-
- def get_queryset(self):
- return self.model.objects.get_queryset(
- user=self.request.user,
- perms="change_product",
- )
-
- def post(self, request, *args, **kwargs):
- self.object = self.get_object()
- return super().post(request, *args, **kwargs)
-
- def form_invalid(self, form):
- raise Http404
-
- def get_success_url(self):
- return f"{self.object.get_absolute_url()}#imports"
-
- def form_valid(self, form):
- self.object = self.get_object()
-
- try:
- form.submit(product=self.object, user=self.request.user)
- except ValidationError as error:
- messages.error(self.request, error)
- return redirect(self.object.get_absolute_url())
-
- msg = "Packages import from ScanCode.io in progress..."
- messages.success(self.request, msg)
- return super().form_valid(form)