Skip to content

Commit

Permalink
Add support to import packages from manifest #65 (#67)
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez authored Mar 29, 2024
1 parent eb2c4ba commit 024697b
Show file tree
Hide file tree
Showing 15 changed files with 325 additions and 121 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 5 additions & 3 deletions dejacode_toolkit/scancodeio.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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)
Expand Down
18 changes: 10 additions & 8 deletions dje/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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
Expand Down
37 changes: 37 additions & 0 deletions product_portfolio/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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."""

Expand Down Expand Up @@ -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):
"""
Expand Down
29 changes: 25 additions & 4 deletions product_portfolio/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -627,21 +635,34 @@ 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"),
created_by=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:
Expand Down
21 changes: 19 additions & 2 deletions product_portfolio/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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)

Expand Down
4 changes: 3 additions & 1 deletion product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
@@ -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 %}
<div class="header">
<div class="header-body">
<div class="row align-items-center">
<div class="col">
<div class="header-pretitle">
<a href="{% inject_preserved_filters 'product_portfolio:product_list' %}">{% trans "Products" %}</a>
/ {{ object.get_absolute_link }}
</div>
<h1 class="header-title">
{% trans "Import Packages from manifests" %}
</h1>
</div>
</div>
</div>
</div>

{% include 'includes/messages_alert.html' %}

<div class="alert alert-success">
<div>
Supports resolving packages for:
<ul class="mt-2">
<li><strong>Python</strong>: requirements.txt and setup.py manifest files.</li>
</ul>
</div>
<strong>Multiple Manifests:</strong>
You can provide multiple Manifests by packaging them into a <strong>zip archive</strong>.
DejaCode will handle and process them accordingly.
</div>

<div class="alert alert-primary" role="alert">
When you upload your <strong>Manifest file to DejaCode</strong>,
the following process will occur:
<ul class="mb-0 mt-2">
<li>
<strong>Submission to ScanCode.io</strong>
Your Manifest file will be submitted to ScanCode.io for thorough scan inspection.
</li>
<li>
<strong>Package Discovery</strong>
ScanCode.io will identify and discover packages within your Manifest.
</li>
<li>
<strong>Package Importation</strong>
DejaCode will retrieve the discovered packages from ScanCode.io and import them into its system.
</li>
<li>
<strong>Package Assignment</strong>
The imported packages will be assigned to the corresponding product within DejaCode.
</li>
</ul>
</div>

<div class="row">
<div class="col-8">
{{ form.errors }}
{% crispy form %}
</div>
</div>
{% endblock %}

{% block javascripts %}
<script>
$(document).ready(function () {
$('form#import-manifest-form').on('submit', function () {
NEXB.displayOverlay("Load Packages from Manifest...");
})
});
</script>
{% endblock %}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
<a class="dropdown-item" href="{{ object.get_import_from_scan_url }}"><i class="fas fa-file-upload"></i> {% trans 'Import data from Scan' %}</a>
{% if request.user.dataspace.enable_package_scanning %}
<a class="dropdown-item" href="{{ object.get_load_sboms_url }}"><i class="fas fa-file-upload"></i> {% trans 'Load Packages from SBOMs' %}</a>
<a class="dropdown-item" href="{{ object.get_import_manifests_url }}"><i class="fas fa-file-upload"></i> {% trans 'Import Packages from manifests' %}</a>
{% endif %}
{% if pull_project_data_form %}
<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>
Expand Down
Loading

0 comments on commit 024697b

Please sign in to comment.