Skip to content

Commit 024697b

Browse files
authored
Add support to import packages from manifest #65 (#67)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent eb2c4ba commit 024697b

15 files changed

Lines changed: 325 additions & 121 deletions

File tree

CHANGELOG.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Release notes
3131
- Refactor the "Import manifest" feature as "Load SBOMs".
3232
https://github.com/nexB/dejacode/issues/61
3333

34+
- Add support to import packages from manifest.
35+
https://github.com/nexB/dejacode/issues/65
36+
3437
- Add a vulnerability link to the VulnerableCode app in the Vulnerability tab.
3538
https://github.com/nexB/dejacode/issues/4
3639

dejacode_toolkit/scancodeio.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@ def submit_scan(self, uri, user_uuid, dataspace_uuid):
7676
logger.debug(f'{self.label}: submit scan uri="{uri}" webhook_url="{webhook_url}"')
7777
return self.request_post(url=self.project_api_url, json=data)
7878

79-
def submit_load_sbom(self, project_name, file_location, user_uuid, execute_now=False):
79+
def submit_project(
80+
self, project_name, pipeline_name, file_location, user_uuid, execute_now=False
81+
):
8082
data = {
8183
"name": project_name,
82-
"pipeline": "load_sbom",
84+
"pipeline": pipeline_name,
8385
"execute_now": execute_now,
8486
}
8587
files = {
@@ -92,7 +94,7 @@ def submit_load_sbom(self, project_name, file_location, user_uuid, execute_now=F
9294
data["webhook_url"] = webhook_url
9395

9496
logger.debug(
95-
f"{self.label}: submit load sbom "
97+
f"{self.label}: submit pipeline={pipeline_name} "
9698
f'project_name="{project_name}" webhook_url="{webhook_url}"'
9799
)
98100
return self.request_post(url=self.project_api_url, data=data, files=files)

dje/tasks.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -115,13 +115,14 @@ def scancodeio_submit_scan(uris, user_uuid, dataspace_uuid):
115115

116116

117117
@job
118-
def scancodeio_submit_load_sbom(scancodeproject_uuid, user_uuid):
118+
def scancodeio_submit_project(scancodeproject_uuid, user_uuid, pipeline_name):
119119
"""Submit the provided SBOM file to ScanCode.io as an asynchronous task."""
120120
from dje.models import DejacodeUser
121121

122122
logger.info(
123-
f"Entering scancodeio_submit_load_sbom task with "
124-
f"scancodeproject_uuid={scancodeproject_uuid} user_uuid={user_uuid}"
123+
f"Entering scancodeio_submit_project task with "
124+
f"scancodeproject_uuid={scancodeproject_uuid} user_uuid={user_uuid} "
125+
f"pipeline_name={pipeline_name}"
125126
)
126127

127128
ScanCodeProject = apps.get_model("product_portfolio", "scancodeproject")
@@ -137,24 +138,25 @@ def scancodeio_submit_load_sbom(scancodeproject_uuid, user_uuid):
137138
# Create a Project instance on ScanCode.io without immediate execution of the
138139
# pipeline. This allows to get instant feedback from ScanCode.io about the Project
139140
# creation status and its related data, even in SYNC mode.
140-
response = scancodeio.submit_load_sbom(
141+
response = scancodeio.submit_project(
141142
project_name=scancodeproject_uuid,
143+
pipeline_name=pipeline_name,
142144
file_location=scancode_project.input_file.path,
143145
user_uuid=user_uuid,
144146
execute_now=False,
145147
)
146148

147149
if not response:
148-
logger.info("Error submitting the SBOM file to ScanCode.io server")
150+
logger.info("Error submitting the file to ScanCode.io server")
149151
scancode_project.status = ScanCodeProject.Status.FAILURE
150-
msg = "- Error: SBOM could not be submitted to ScanCode.io"
152+
msg = "- Error: File could not be submitted to ScanCode.io"
151153
scancode_project.append_to_log(msg, save=True)
152154
return
153155

154156
logger.info("Update the ScanCodeProject instance")
155157
scancode_project.status = ScanCodeProject.Status.SUBMITTED
156158
scancode_project.project_uuid = response.get("uuid")
157-
msg = "- SBOM file submitted to ScanCode.io for inspection"
159+
msg = "- File submitted to ScanCode.io for inspection"
158160
scancode_project.append_to_log(msg, save=True)
159161

160162
# Delay the execution of the pipeline after the ScancodeProject instance was
@@ -164,7 +166,7 @@ def scancodeio_submit_load_sbom(scancodeproject_uuid, user_uuid):
164166
transaction.on_commit(lambda: scancodeio.start_pipeline(run_url=runs[0]["url"]))
165167

166168

167-
@job
169+
@job("default", timeout=1200)
168170
def pull_project_data_from_scancodeio(scancodeproject_uuid):
169171
"""
170172
Pull Project data from ScanCode.io as an asynchronous task for the provided

product_portfolio/api.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
from dje.permissions import assign_all_object_permissions
3636
from product_portfolio.filters import ComponentCompletenessAPIFilter
3737
from product_portfolio.forms import ImportFromScanForm
38+
from product_portfolio.forms import ImportManifestsForm
3839
from product_portfolio.forms import LoadSBOMsForm
3940
from product_portfolio.forms import PullProjectDataForm
4041
from product_portfolio.models import CodebaseResource
@@ -216,6 +217,25 @@ class LoadSBOMsFormSerializer(serializers.Serializer):
216217
)
217218

218219

220+
class ImportManifestsFormSerializer(serializers.Serializer):
221+
"""Serializer equivalent of ImportManifestsForm, used for API documentation."""
222+
223+
input_file = serializers.FileField(
224+
required=True,
225+
help_text=ImportManifestsForm.base_fields["input_file"].label,
226+
)
227+
update_existing_packages = serializers.BooleanField(
228+
required=False,
229+
default=False,
230+
help_text=ImportManifestsForm.base_fields["update_existing_packages"].help_text,
231+
)
232+
scan_all_packages = serializers.BooleanField(
233+
required=False,
234+
default=False,
235+
help_text=ImportManifestsForm.base_fields["scan_all_packages"].help_text,
236+
)
237+
238+
219239
class ImportFromScanSerializer(serializers.Serializer):
220240
"""Serializer equivalent of ImportFromScanForm, used for API documentation."""
221241

@@ -319,6 +339,23 @@ def load_sboms(self, request, *args, **kwargs):
319339
form.submit(product=product, user=request.user)
320340
return Response({"status": "SBOM file submitted to ScanCode.io for inspection."})
321341

342+
@action(detail=True, methods=["post"], serializer_class=ImportManifestsFormSerializer)
343+
def import_manifests(self, request, *args, **kwargs):
344+
"""
345+
Import Packages from Manifests.
346+
347+
Multiple Manifests: You can provide multiple files by packaging them into a zip
348+
archive. DejaCode will handle and process them accordingly.
349+
"""
350+
product = self.get_object()
351+
352+
form = ImportManifestsForm(data=request.POST, files=request.FILES)
353+
if not form.is_valid():
354+
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
355+
356+
form.submit(product=product, user=request.user)
357+
return Response({"status": "Manifest file submitted to ScanCode.io for inspection."})
358+
322359
@action(detail=True, methods=["post"], serializer_class=ImportFromScanSerializer)
323360
def import_from_scan(self, request, *args, **kwargs):
324361
"""

product_portfolio/forms.py

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -587,11 +587,15 @@ def save(self, product):
587587
return warnings, created_counts
588588

589589

590-
class LoadSBOMsForm(forms.Form):
590+
class BaseProductImportFormView(forms.Form):
591+
project_type = None
592+
input_label = ""
593+
591594
input_file = SmartFileField(
592-
label=_("SBOM file or zip archive"),
595+
label=_("file or zip archive"),
593596
required=True,
594597
)
598+
595599
update_existing_packages = forms.BooleanField(
596600
label=_("Update existing packages with discovered packages data"),
597601
required=False,
@@ -614,6 +618,10 @@ class LoadSBOMsForm(forms.Form):
614618
),
615619
)
616620

621+
def __init__(self, *args, **kwargs):
622+
super().__init__(*args, **kwargs)
623+
self.fields["input_file"].label = _(f"{self.input_label} file or zip archive")
624+
617625
@property
618626
def helper(self):
619627
helper = FormHelper()
@@ -627,21 +635,34 @@ def submit(self, product, user):
627635
scancode_project = ScanCodeProject.objects.create(
628636
product=product,
629637
dataspace=product.dataspace,
630-
type=ScanCodeProject.ProjectType.LOAD_SBOMS,
638+
type=self.project_type,
631639
input_file=self.cleaned_data.get("input_file"),
632640
update_existing_packages=self.cleaned_data.get("update_existing_packages"),
633641
scan_all_packages=self.cleaned_data.get("scan_all_packages"),
634642
created_by=user,
635643
)
636644

637645
transaction.on_commit(
638-
lambda: tasks.scancodeio_submit_load_sbom.delay(
646+
lambda: tasks.scancodeio_submit_project.delay(
639647
scancodeproject_uuid=scancode_project.uuid,
640648
user_uuid=user.uuid,
649+
pipeline_name=self.pipeline_name,
641650
)
642651
)
643652

644653

654+
class LoadSBOMsForm(BaseProductImportFormView):
655+
project_type = ScanCodeProject.ProjectType.LOAD_SBOMS
656+
input_label = "SBOM"
657+
pipeline_name = "load_sbom"
658+
659+
660+
class ImportManifestsForm(BaseProductImportFormView):
661+
project_type = ScanCodeProject.ProjectType.IMPORT_FROM_MANIFEST
662+
input_label = "Manifest"
663+
pipeline_name = "resolve_dependencies"
664+
665+
645666
class StrongTextWidget(forms.Widget):
646667
def render(self, name, value, attrs=None, renderer=None):
647668
if value:

product_portfolio/importers.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from django.core.exceptions import MultipleObjectsReturned
1414
from django.core.exceptions import ValidationError
1515
from django.core.validators import EMPTY_VALUES
16+
from django.db import IntegrityError
1617
from django.db import transaction
1718
from django.db.models import ObjectDoesNotExist
1819
from django.db.models import Q
@@ -26,11 +27,13 @@
2627
from component_catalog.models import Component
2728
from component_catalog.models import Package
2829
from dejacode_toolkit.scancodeio import ScanCodeIO
30+
from dje.copier import copy_object
2931
from dje.importers import BaseImporter
3032
from dje.importers import BaseImportModelForm
3133
from dje.importers import BaseImportModelFormSet
3234
from dje.importers import ComponentRelatedFieldImportMixin
3335
from dje.importers import ModelChoiceFieldForImport
36+
from dje.models import Dataspace
3437
from dje.utils import get_help_text
3538
from dje.utils import is_uuid4
3639
from product_portfolio.forms import ProductComponentLicenseExpressionFormMixin
@@ -667,12 +670,26 @@ def import_package(self, package_data):
667670
if (value := package_data.get(field))
668671
}
669672

673+
# Check if the Package already exists in the local Dataspace
670674
try:
671675
package = Package.objects.scope(self.user.dataspace).get(**unique_together_lookups)
672676
self.existing.append(package)
673677
except (ObjectDoesNotExist, MultipleObjectsReturned):
674678
package = None
675679

680+
# Check if the Package already exists in the reference Dataspace
681+
reference_dataspace = Dataspace.objects.get_reference()
682+
user_dataspace = self.user.dataspace
683+
if not package and user_dataspace != reference_dataspace:
684+
qs = Package.objects.scope(reference_dataspace).filter(**unique_together_lookups)
685+
if qs.exists():
686+
reference_object = qs.first()
687+
try:
688+
package = copy_object(reference_object, user_dataspace, self.user, update=False)
689+
self.created.append(package)
690+
except IntegrityError as error:
691+
self.errors.append(error)
692+
676693
if license_expression := package_data.get("declared_license_expression"):
677694
license_expression = str(self.licensing.dedup(license_expression))
678695
package_data["license_expression"] = license_expression
@@ -683,8 +700,8 @@ def import_package(self, package_data):
683700
if not package:
684701
try:
685702
package = Package.create_from_data(self.user, package_data, validate=True)
686-
except ValidationError as e:
687-
self.errors.append(e)
703+
except ValidationError as errors:
704+
self.errors.append(errors)
688705
return
689706
self.created.append(package)
690707

product_portfolio/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,9 @@ def get_check_package_version_url(self):
254254
def get_load_sboms_url(self):
255255
return self.get_url("load_sboms")
256256

257+
def get_import_manifests_url(self):
258+
return self.get_url("import_manifests")
259+
257260
def get_pull_project_data_url(self):
258261
return self.get_url("pull_project_data")
259262

@@ -1119,7 +1122,6 @@ class ScanCodeProject(HistoryFieldsMixin, DataspacedModel):
11191122
"""Wrap a ScanCode.io Project."""
11201123

11211124
class ProjectType(models.TextChoices):
1122-
# This type was replaced by LOAD_SBOMS but is kept for backward compatibility
11231125
IMPORT_FROM_MANIFEST = "IMPORT_FROM_MANIFEST", _("Import from Manifest")
11241126
LOAD_SBOMS = "LOAD_SBOMS", _("Load SBOMs")
11251127
PULL_FROM_SCANCODEIO = "PULL_FROM_SCANCODEIO", _("Pull from ScanCode.io")
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
{% extends "bootstrap_base.html" %}
2+
{% load i18n static crispy_forms_tags %}
3+
{% load inject_preserved_filters from dje_tags %}
4+
5+
{% block page_title %}{% trans "Import Packages from manifests" %}{% endblock %}
6+
7+
{% block content %}
8+
<div class="header">
9+
<div class="header-body">
10+
<div class="row align-items-center">
11+
<div class="col">
12+
<div class="header-pretitle">
13+
<a href="{% inject_preserved_filters 'product_portfolio:product_list' %}">{% trans "Products" %}</a>
14+
/ {{ object.get_absolute_link }}
15+
</div>
16+
<h1 class="header-title">
17+
{% trans "Import Packages from manifests" %}
18+
</h1>
19+
</div>
20+
</div>
21+
</div>
22+
</div>
23+
24+
{% include 'includes/messages_alert.html' %}
25+
26+
<div class="alert alert-success">
27+
<div>
28+
Supports resolving packages for:
29+
<ul class="mt-2">
30+
<li><strong>Python</strong>: requirements.txt and setup.py manifest files.</li>
31+
</ul>
32+
</div>
33+
<strong>Multiple Manifests:</strong>
34+
You can provide multiple Manifests by packaging them into a <strong>zip archive</strong>.
35+
DejaCode will handle and process them accordingly.
36+
</div>
37+
38+
<div class="alert alert-primary" role="alert">
39+
When you upload your <strong>Manifest file to DejaCode</strong>,
40+
the following process will occur:
41+
<ul class="mb-0 mt-2">
42+
<li>
43+
<strong>Submission to ScanCode.io</strong>
44+
Your Manifest file will be submitted to ScanCode.io for thorough scan inspection.
45+
</li>
46+
<li>
47+
<strong>Package Discovery</strong>
48+
ScanCode.io will identify and discover packages within your Manifest.
49+
</li>
50+
<li>
51+
<strong>Package Importation</strong>
52+
DejaCode will retrieve the discovered packages from ScanCode.io and import them into its system.
53+
</li>
54+
<li>
55+
<strong>Package Assignment</strong>
56+
The imported packages will be assigned to the corresponding product within DejaCode.
57+
</li>
58+
</ul>
59+
</div>
60+
61+
<div class="row">
62+
<div class="col-8">
63+
{{ form.errors }}
64+
{% crispy form %}
65+
</div>
66+
</div>
67+
{% endblock %}
68+
69+
{% block javascripts %}
70+
<script>
71+
$(document).ready(function () {
72+
$('form#import-manifest-form').on('submit', function () {
73+
NEXB.displayOverlay("Load Packages from Manifest...");
74+
})
75+
});
76+
</script>
77+
{% endblock %}

product_portfolio/templates/product_portfolio/product_details.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<a class="dropdown-item" href="{{ object.get_import_from_scan_url }}"><i class="fas fa-file-upload"></i> {% trans 'Import data from Scan' %}</a>
4040
{% if request.user.dataspace.enable_package_scanning %}
4141
<a class="dropdown-item" href="{{ object.get_load_sboms_url }}"><i class="fas fa-file-upload"></i> {% trans 'Load Packages from SBOMs' %}</a>
42+
<a class="dropdown-item" href="{{ object.get_import_manifests_url }}"><i class="fas fa-file-upload"></i> {% trans 'Import Packages from manifests' %}</a>
4243
{% endif %}
4344
{% if pull_project_data_form %}
4445
<a class="dropdown-item" style="margin-left: -3px;" href="#" data-bs-toggle="modal" data-bs-target="#pull-project-data-modal"><i class="fas fa-cloud-download-alt"></i> {% trans 'Pull ScanCode.io Project data' %}</a>

0 commit comments

Comments
 (0)