From 82f7ce6112620ba43e9047c4edf81650fa4f19ad Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Wed, 20 Mar 2024 17:20:59 +0530
Subject: [PATCH 01/12] Add initial LicenseDetection Models and UI
Signed-off-by: Ayan Sinha Mahapatra
---
scancodeio/settings.py | 1 +
scanpipe/api/serializers.py | 16 ++
scanpipe/filters.py | 49 ++++++
.../0054_discovered_license_models.py | 141 +++++++++++++++++
scanpipe/models.py | 145 ++++++++++++++++++
.../scanpipe/license_detection_detail.html | 13 ++
.../scanpipe/license_detection_list.html | 59 +++++++
scanpipe/urls.py | 10 ++
scanpipe/views.py | 74 +++++++++
9 files changed, 508 insertions(+)
create mode 100644 scanpipe/migrations/0054_discovered_license_models.py
create mode 100644 scanpipe/templates/scanpipe/license_detection_detail.html
create mode 100644 scanpipe/templates/scanpipe/license_detection_list.html
diff --git a/scancodeio/settings.py b/scancodeio/settings.py
index 112de2def..bab8f3ca0 100644
--- a/scancodeio/settings.py
+++ b/scancodeio/settings.py
@@ -108,6 +108,7 @@
"resource": 100,
"package": 100,
"dependency": 100,
+ "license": 100,
"relation": 100,
},
)
diff --git a/scanpipe/api/serializers.py b/scanpipe/api/serializers.py
index 5f4173246..f1e9ad88c 100644
--- a/scanpipe/api/serializers.py
+++ b/scanpipe/api/serializers.py
@@ -31,6 +31,7 @@
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredPackage
+from scanpipe.models import DiscoveredLicense
from scanpipe.models import InputSource
from scanpipe.models import Project
from scanpipe.models import ProjectMessage
@@ -413,6 +414,20 @@ class Meta:
]
+class DiscoveredLicenseSerializer(serializers.ModelSerializer):
+ compliance_alert = serializers.CharField()
+
+ class Meta:
+ model = DiscoveredLicense
+ fields = [
+ "detection_count",
+ "identifier",
+ "license_expression",
+ "license_expression_spdx",
+ "compliance_alert",
+ ]
+
+
class CodebaseRelationSerializer(serializers.ModelSerializer):
from_resource = serializers.ReadOnlyField(source="from_resource.path")
to_resource = serializers.ReadOnlyField(source="to_resource.path")
@@ -471,6 +486,7 @@ def get_model_serializer(model_class):
CodebaseResource: CodebaseResourceSerializer,
DiscoveredPackage: DiscoveredPackageSerializer,
DiscoveredDependency: DiscoveredDependencySerializer,
+ DiscoveredLicense: DiscoveredLicenseSerializer,
CodebaseRelation: CodebaseRelationSerializer,
ProjectMessage: ProjectMessageSerializer,
}.get(model_class, None)
diff --git a/scanpipe/filters.py b/scanpipe/filters.py
index cdba64d8a..92ef8e635 100644
--- a/scanpipe/filters.py
+++ b/scanpipe/filters.py
@@ -39,6 +39,7 @@
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredPackage
+from scanpipe.models import DiscoveredLicense
from scanpipe.models import Project
from scanpipe.models import ProjectMessage
from scanpipe.models import Run
@@ -591,6 +592,19 @@ def filter(self, qs, value):
return qs.filter(lookups)
+class DiscoveredLicenseSearchFilter(QuerySearchFilter):
+ def filter(self, qs, value):
+ if not value:
+ return qs
+
+ search_fields = ["license_expression", "license_expression_spdx"]
+ lookups = Q()
+ for field_names in search_fields:
+ lookups |= Q(**{f"{field_names}__{self.lookup_expr}": value})
+
+ return qs.filter(lookups)
+
+
class GroupOrderingFilter(django_filters.OrderingFilter):
"""Add the ability to provide a group a fields to order by."""
@@ -755,6 +769,41 @@ class Meta:
]
+class LicenseFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
+ dropdown_widget_fields = [
+ "compliance_alert",
+ ]
+
+ search = DiscoveredLicenseSearchFilter(
+ label="Search", field_name="name", lookup_expr="icontains"
+ )
+ sort = GroupOrderingFilter(
+ label="Sort",
+ fields=[
+ "detection_count",
+ "identifier",
+ "license_expression",
+ "license_expression_spdx",
+ "compliance_alert",
+ ],
+ )
+ license_expression = ParentAllValuesFilter()
+ compliance_alert = django_filters.ChoiceFilter(
+ choices=[(EMPTY_VAR, "None")] + CodebaseResource.Compliance.choices,
+ )
+
+ class Meta:
+ model = DiscoveredLicense
+ fields = [
+ "search",
+ "identifier",
+ "detection_count",
+ "license_expression",
+ "license_expression_spdx",
+ "compliance_alert",
+ ]
+
+
class ProjectMessageFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
search = QuerySearchFilter(
label="Search", field_name="description", lookup_expr="icontains"
diff --git a/scanpipe/migrations/0054_discovered_license_models.py b/scanpipe/migrations/0054_discovered_license_models.py
new file mode 100644
index 000000000..d4253eb6f
--- /dev/null
+++ b/scanpipe/migrations/0054_discovered_license_models.py
@@ -0,0 +1,141 @@
+# Generated by Django 5.0.2 on 2024-03-17 12:38
+
+import django.db.models.deletion
+import scanpipe.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ("scanpipe", "0053_restructure_pipelines_data"),
+ ]
+
+ operations = [
+ migrations.CreateModel(
+ name="DiscoveredLicense",
+ fields=[
+ (
+ "id",
+ models.AutoField(
+ auto_created=True,
+ primary_key=True,
+ serialize=False,
+ verbose_name="ID",
+ ),
+ ),
+ (
+ "compliance_alert",
+ models.CharField(
+ blank=True,
+ choices=[
+ ("ok", "Ok"),
+ ("warning", "Warning"),
+ ("error", "Error"),
+ ("missing", "Missing"),
+ ],
+ editable=False,
+ help_text="Indicates how the license expression complies with provided policies.",
+ max_length=10,
+ ),
+ ),
+ (
+ "license_expression",
+ models.TextField(
+ blank=True,
+ help_text="A license expression string using the SPDX license expression syntax and ScanCode license keys, the effective license expression for this license detection.",
+ ),
+ ),
+ (
+ "license_expression_spdx",
+ models.TextField(
+ blank=True,
+ help_text="SPDX license expression string with SPDX ids.",
+ ),
+ ),
+ (
+ "matches",
+ models.JSONField(
+ blank=True,
+ default=list,
+ help_text="List of license matches combined in this detection.",
+ ),
+ ),
+ (
+ "detection_log",
+ models.JSONField(
+ blank=True,
+ default=list,
+ help_text="A list of detection DetectionRule explaining how this detection was created.",
+ ),
+ ),
+ (
+ "identifier",
+ models.CharField(
+ blank=True,
+ help_text="An identifier unique for a license detection, containing the license expression and a UUID crafted from the match contents.",
+ max_length=1024,
+ ),
+ ),
+ (
+ "detection_count",
+ models.BigIntegerField(
+ blank=True,
+ help_text="Total number of this license detection discovered.",
+ null=True,
+ ),
+ ),
+ (
+ "file_regions",
+ models.JSONField(
+ blank=True,
+ default=list,
+ help_text="A list of file regions with resource path, start and end line details for each place this license detection was discovered at. Also contains whether this license was discovered from a file or from package metadata.",
+ ),
+ ),
+ (
+ "project",
+ models.ForeignKey(
+ editable=False,
+ on_delete=django.db.models.deletion.CASCADE,
+ related_name="%(class)ss",
+ to="scanpipe.project",
+ ),
+ ),
+ ],
+ options={
+ "ordering": ["detection_count", "identifier"],
+ "indexes": [
+ models.Index(
+ fields=["identifier"], name="scanpipe_di_identif_b533f3_idx"
+ ),
+ models.Index(
+ fields=["license_expression"],
+ name="scanpipe_di_license_33d11a_idx",
+ ),
+ models.Index(
+ fields=["license_expression_spdx"],
+ name="scanpipe_di_license_eb5e9d_idx",
+ ),
+ models.Index(
+ fields=["detection_count"],
+ name="scanpipe_di_detecti_d87ff1_idx",
+ ),
+ ],
+ },
+ bases=(
+ scanpipe.models.UpdateMixin,
+ scanpipe.models.SaveProjectMessageMixin,
+ scanpipe.models.UpdateFromDataMixin,
+ models.Model,
+ ),
+ ),
+ migrations.AddConstraint(
+ model_name="discoveredlicense",
+ constraint=models.UniqueConstraint(
+ condition=models.Q(("identifier", ""), _negated=True),
+ fields=("project", "identifier"),
+ name="scanpipe_discoveredlicense_unique_license_id_within_project",
+ ),
+ ),
+ ]
diff --git a/scanpipe/models.py b/scanpipe/models.py
index 412fb985d..b0f9c9d41 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -612,6 +612,7 @@ def delete_related_objects(self):
self.projectmessages,
self.codebaserelations,
self.discovereddependencies,
+ self.discoveredlicenses,
self.codebaseresources,
self.runs,
]
@@ -3473,6 +3474,150 @@ def as_spdx(self):
)
+class DiscoveredLicenseQuerySet(ProjectRelatedQuerySet):
+ def order_by_count_and_expression(self):
+ """Order by detection count and license expression (identifer) fields."""
+ return self.order_by("detection_count", "identifier")
+
+
+class AbstractLicenseDetection(models.Model):
+ """
+ These fields should be kept in line with
+ `licensedcode.detection.LicenseDetection`.
+ """
+
+ license_expression = models.TextField(
+ blank=True,
+ help_text=_(
+ 'A license expression string using the SPDX license expression'
+ ' syntax and ScanCode license keys, the effective license expression'
+ ' for this license detection.'
+ ),
+ )
+
+ license_expression_spdx = models.TextField(
+ blank=True,
+ help_text=_('SPDX license expression string with SPDX ids.'),
+ )
+
+ matches = models.JSONField(
+ default=list,
+ blank=True,
+ help_text=_('List of license matches combined in this detection.'),
+ )
+
+ detection_log = models.JSONField(
+ default=list,
+ blank=True,
+ help_text=_(
+ 'A list of detection DetectionRule explaining how '
+ 'this detection was created.'
+ ),
+ )
+
+ identifier = models.CharField(
+ max_length=1024,
+ blank=True,
+ help_text=_(
+ 'An identifier unique for a license detection, containing the license '
+ 'expression and a UUID crafted from the match contents.'
+ ),
+ )
+
+ class Meta:
+ abstract = True
+
+
+class DiscoveredLicense(
+ ProjectRelatedModel,
+ SaveProjectMessageMixin,
+ UpdateFromDataMixin,
+ ComplianceAlertMixin,
+ AbstractLicenseDetection,
+):
+ """
+ A project's Discovered Licenses are the unique License Detection objects
+ discovered in the code under analysis.
+
+ """
+ license_expression_field = "license_expression"
+
+ detection_count = models.BigIntegerField(
+ blank=True,
+ null=True,
+ help_text=_("Total number of this license detection discovered."),
+ )
+
+ file_regions = models.JSONField(
+ default=list,
+ blank=True,
+ help_text=_(
+ 'A list of file regions with resource path, start and end line '
+ 'details for each place this license detection was discovered at. '
+ 'Also contains whether this license was discovered from a file or '
+ 'from package metadata.'
+ ),
+ )
+
+ objects = DiscoveredLicenseQuerySet.as_manager()
+
+ class Meta:
+ ordering = ["detection_count", "identifier"]
+ indexes = [
+ models.Index(fields=["identifier"]),
+ models.Index(fields=["license_expression"]),
+ models.Index(fields=["license_expression_spdx"]),
+ models.Index(fields=["detection_count"]),
+ ]
+ constraints = [
+ models.UniqueConstraint(
+ fields=["project", "identifier"],
+ condition=~Q(identifier=""),
+ name="%(app_label)s_%(class)s_unique_license_id_within_project",
+ ),
+ ]
+
+ def __str__(self):
+ return self.identifier
+
+ @classmethod
+ def create_from_data(cls, project, detection_data):
+ """
+ Create and returns a DiscoveredLicense for a `project` from the `detection_data`.
+ If one of the values of the required fields is not available, a "ProjectMessage"
+ is created instead of a new DiscoveredLicense instance.
+ """
+ detection_data = detection_data.copy()
+ required_fields = ["license_expression", "identifier", "matches"]
+ missing_values = [
+ field_name
+ for field_name in required_fields
+ if not detection_data.get(field_name)
+ ]
+
+ if missing_values:
+ message = (
+ f"No values for the following required fields: "
+ f"{', '.join(missing_values)}"
+ )
+
+ project.add_warning(description=message, model=cls, details=detection_data)
+ return
+
+ cleaned_data = {
+ field_name: value
+ for field_name, value in detection_data.items()
+ if field_name in cls.model_fields() and value not in EMPTY_VALUES
+ }
+
+ discovered_license = cls(project=project, **cleaned_data)
+ # Using save_error=False to not capture potential errors at this level but
+ # rather in the CodebaseResource.create_and_add_license_data method so resource data
+ # can be injected in the ProjectMessage record.
+ discovered_license.save(save_error=False, capture_exception=False)
+ return discovered_license
+
+
class WebhookSubscription(UUIDPKModel, ProjectRelatedModel):
target_url = models.URLField(_("Target URL"), max_length=1024)
created_date = models.DateTimeField(auto_now_add=True, editable=False)
diff --git a/scanpipe/templates/scanpipe/license_detection_detail.html b/scanpipe/templates/scanpipe/license_detection_detail.html
new file mode 100644
index 000000000..eec34ad59
--- /dev/null
+++ b/scanpipe/templates/scanpipe/license_detection_detail.html
@@ -0,0 +1,13 @@
+{% extends "scanpipe/base.html" %}
+{% load static %}
+
+{% block title %}ScanCode.io: {{ project.name }} - {{ object.name }}{% endblock %}
+
+{% block content %}
+
+ {% include 'scanpipe/includes/navbar_header.html' %}
+
{% include 'scanpipe/includes/messages.html' %}
+ {% include 'scanpipe/includes/breadcrumb_detail_view.html' with object_title=object.identifier url_name="project_licenses" %}
+ {% include 'scanpipe/tabset/tabset.html' %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/license_detection_list.html b/scanpipe/templates/scanpipe/license_detection_list.html
new file mode 100644
index 000000000..426c5b145
--- /dev/null
+++ b/scanpipe/templates/scanpipe/license_detection_list.html
@@ -0,0 +1,59 @@
+{% extends "scanpipe/base.html" %}
+
+{% block title %}ScanCode.io: {{ project.name }} - License Detections{% endblock %}
+
+{% block content %}
+
+
+
+
+
+ {% if is_paginated %}
+ {% include 'scanpipe/includes/pagination.html' with page_obj=page_obj %}
+ {% endif %}
+
+{% endblock %}
\ No newline at end of file
diff --git a/scanpipe/urls.py b/scanpipe/urls.py
index 3391c00c4..5469c7e1a 100644
--- a/scanpipe/urls.py
+++ b/scanpipe/urls.py
@@ -51,6 +51,11 @@
views.DiscoveredPackageDetailsView.as_view(),
name="package_detail",
),
+ path(
+ "project//license_detections//",
+ views.DiscoveredLicenseDetailsView.as_view(),
+ name="license_detail",
+ ),
path(
"project//dependencies//",
views.DiscoveredDependencyDetailsView.as_view(),
@@ -61,6 +66,11 @@
views.DiscoveredPackageListView.as_view(),
name="project_packages",
),
+ path(
+ "project//license_detections/",
+ views.DiscoveredLicenseListView.as_view(),
+ name="project_licenses",
+ ),
path(
"project//dependencies/",
views.DiscoveredDependencyListView.as_view(),
diff --git a/scanpipe/views.py b/scanpipe/views.py
index 22dd676fe..ec0afe5fb 100644
--- a/scanpipe/views.py
+++ b/scanpipe/views.py
@@ -65,6 +65,7 @@
from scanpipe.filters import PAGE_VAR
from scanpipe.filters import DependencyFilterSet
from scanpipe.filters import PackageFilterSet
+from scanpipe.filters import LicenseFilterSet
from scanpipe.filters import ProjectFilterSet
from scanpipe.filters import ProjectMessageFilterSet
from scanpipe.filters import RelationFilterSet
@@ -82,6 +83,7 @@
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredPackage
+from scanpipe.models import DiscoveredLicense
from scanpipe.models import InputSource
from scanpipe.models import Project
from scanpipe.models import ProjectMessage
@@ -1517,6 +1519,46 @@ def get_queryset(self):
return super().get_queryset().order_by("dependency_uid")
+class DiscoveredLicenseListView(
+ ConditionalLoginRequired,
+ ProjectRelatedViewMixin,
+ TableColumnsMixin,
+ ExportXLSXMixin,
+ PaginatedFilterView,
+):
+ model = DiscoveredLicense
+ filterset_class = LicenseFilterSet
+ template_name = "scanpipe/license_detection_list.html"
+ paginate_by = settings.SCANCODEIO_PAGINATE_BY.get("license", 10)
+ table_columns = [
+ "identifier",
+ {
+ "field_name": "license_expression",
+ "filter_fieldname": "license_expression",
+ },
+ "license_expression_spdx",
+ "detection_count",
+ {
+ "field_name": "compliance_alert",
+ "condition": scanpipe_app.policies_enabled,
+ "filter_fieldname": "compliance_alert",
+ },
+ ]
+
+ def get_queryset(self):
+ return (
+ super()
+ .get_queryset()
+ .only(
+ "detection_count",
+ "license_expression",
+ "license_expression_spdx",
+ "compliance_alert",
+ )
+ .order_by_count_and_expression()
+ )
+
+
class ProjectMessageListView(
ConditionalLoginRequired,
ProjectRelatedViewMixin,
@@ -1967,6 +2009,38 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["dependency_data"] = DiscoveredDependencySerializer(self.object).data
return context
+
+
+class DiscoveredLicenseDetailsView(
+ ConditionalLoginRequired,
+ ProjectRelatedViewMixin,
+ TabSetMixin,
+ generic.DetailView,
+):
+ model = DiscoveredLicense
+ model_label = "license_detections"
+ slug_field = "identifier"
+ slug_url_kwarg = "identifier"
+ template_name = "scanpipe/license_detection_detail.html"
+ tabset = {
+ "essentials": {
+ "fields": [
+ "license_expression",
+ "license_expression_spdx",
+ "identifier",
+ "detection_count",
+ ],
+ "icon_class": "fa-solid fa-info-circle",
+ },
+ "detection": {
+ "fields": [
+ {"field_name": "matches", "render_func": render_as_yaml},
+ {"field_name": "detection_log", "render_func": render_as_yaml},
+ {"field_name": "file_regions", "render_func": render_as_yaml},
+ ],
+ "icon_class": "fa-solid fa-search",
+ },
+ }
@conditional_login_required
From 7536957d1ccfe032760d9a981c9e8160c1b5baa8 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Wed, 20 Mar 2024 17:21:32 +0530
Subject: [PATCH 02/12] Support LicenseDetection models in load_inventory
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/pipes/__init__.py | 32 ++++++++++++++++++++++++++++++++
scanpipe/pipes/input.py | 15 +++++++++++----
scanpipe/pipes/scancode.py | 19 +++++++++++++++++++
3 files changed, 62 insertions(+), 4 deletions(-)
diff --git a/scanpipe/pipes/__init__.py b/scanpipe/pipes/__init__.py
index 7e8c737f6..be7c7aff8 100644
--- a/scanpipe/pipes/__init__.py
+++ b/scanpipe/pipes/__init__.py
@@ -37,6 +37,7 @@
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredPackage
+from scanpipe.models import DiscoveredLicense
from scanpipe.pipes import scancode
logger = logging.getLogger("scanpipe.pipes")
@@ -245,6 +246,37 @@ def update_or_create_dependency(
return dependency
+def update_or_create_license_detection(project, detection_data):
+ """
+ Get, update or create a DiscoveredLicense object then return it.
+ Use the `project` and `detection_data` mapping to lookup and creates the
+ DiscoveredLicense using its detection identifier as a unique key.
+ """
+ detection_identifier = detection_data["identifier"]
+
+ license_detection = project.discoveredlicenses.get_or_none(
+ identifier=detection_identifier,
+ )
+ detection_data = _clean_license_detection_data(detection_data)
+
+ if license_detection:
+ license_detection.update_from_data(detection_data)
+ else:
+ license_detection = DiscoveredLicense.create_from_data(
+ project,
+ detection_data,
+ )
+
+ return license_detection
+
+
+def _clean_license_detection_data(detection_data):
+ detection_data = detection_data.copy()
+ matches = detection_data.pop("sample_matches")
+ detection_data["matches"] = matches
+ return detection_data
+
+
def get_or_create_relation(project, relation_data):
"""
Get or create a CodebaseRelation then return it.
diff --git a/scanpipe/pipes/input.py b/scanpipe/pipes/input.py
index 56befe451..41e5eb16a 100644
--- a/scanpipe/pipes/input.py
+++ b/scanpipe/pipes/input.py
@@ -35,6 +35,7 @@
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
from scanpipe.models import DiscoveredPackage
+from scanpipe.models import DiscoveredLicense
from scanpipe.pipes import scancode
from scanpipe.pipes.output import mappings_key_by_fieldname
@@ -78,10 +79,11 @@ def is_archive(location):
def load_inventory_from_toolkit_scan(project, input_location):
"""
- Create packages, dependencies, and resources loaded from the ScanCode-toolkit scan
- results located at ``input_location``.
+ Create license detections, packages, dependencies, and resources
+ loaded from the ScanCode-toolkit scan results located at ``input_location``.
"""
scanned_codebase = scancode.get_virtual_codebase(project, input_location)
+ scancode.create_discovered_licenses(project, scanned_codebase)
scancode.create_discovered_packages(project, scanned_codebase)
scancode.create_codebase_resources(project, scanned_codebase)
scancode.create_discovered_dependencies(
@@ -91,9 +93,12 @@ def load_inventory_from_toolkit_scan(project, input_location):
def load_inventory_from_scanpipe(project, scan_data):
"""
- Create packages, dependencies, resources, and relations loaded from a ScanCode.io
- JSON output provided as ``scan_data``.
+ Create license detections, packages, dependencies, resources, and relations
+ loaded from a ScanCode.io JSON output provided as ``scan_data``.
"""
+ for detection_data in scan_data.get("license_detections", []):
+ pipes.update_or_create_license_detection(project, detection_data)
+
for package_data in scan_data.get("packages", []):
pipes.update_or_create_package(project, package_data)
@@ -110,12 +115,14 @@ def load_inventory_from_scanpipe(project, scan_data):
model_to_object_maker_func = {
DiscoveredPackage: pipes.update_or_create_package,
DiscoveredDependency: pipes.update_or_create_dependency,
+ DiscoveredLicense: pipes.update_or_create_license_detection,
CodebaseResource: pipes.update_or_create_resource,
CodebaseRelation: pipes.get_or_create_relation,
}
worksheet_name_to_model = {
"PACKAGES": DiscoveredPackage,
+ "LICENSE_DETECTIONS": DiscoveredLicense,
"RESOURCES": CodebaseResource,
"DEPENDENCIES": DiscoveredDependency,
"RELATIONS": CodebaseRelation,
diff --git a/scanpipe/pipes/scancode.py b/scanpipe/pipes/scancode.py
index 538d68d4e..a1174a221 100644
--- a/scanpipe/pipes/scancode.py
+++ b/scanpipe/pipes/scancode.py
@@ -586,6 +586,14 @@ def create_codebase_resources(project, scanned_codebase):
discovered_package=package,
)
+ license_detections = getattr(scanned_resource, "license_detections", [])
+ for detection_data in license_detections:
+ detection_identifier = detection_data.get("identifier")
+ license_detection = project.discoveredlicenses.get_or_none(
+ identifier=detection_identifier
+ )
+ logger.debug(f"Add {codebase_resource} to {license_detection}")
+
def create_discovered_packages(project, scanned_codebase):
"""
@@ -620,6 +628,17 @@ def create_discovered_dependencies(
)
+def create_discovered_licenses(project, scanned_codebase):
+ """
+ Save the license detections of a ScanCode `scanned_codebase`
+ scancode.resource.Codebase object to the database as a DiscoveredLicense of
+ `project`.
+ """
+ if hasattr(scanned_codebase.attributes, "license_detections"):
+ for detection_data in scanned_codebase.attributes.license_detections:
+ pipes.update_or_create_license_detection(project, detection_data)
+
+
def set_codebase_resource_for_package(codebase_resource, discovered_package):
"""
Assign the `discovered_package` to the `codebase_resource` and set its
From 1a32a895ab113ed5165e424ea55584211480beed Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Thu, 21 Mar 2024 15:38:45 +0530
Subject: [PATCH 03/12] Add License Detections in project summary
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/models.py | 7 ++++++-
.../scanpipe/includes/project_summary_level.html | 14 ++++++++++++++
2 files changed, 20 insertions(+), 1 deletion(-)
diff --git a/scanpipe/models.py b/scanpipe/models.py
index b0f9c9d41..44d72fb46 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -1229,6 +1229,11 @@ def dependency_count(self):
"""Return the number of dependencies related to this project."""
return self.discovereddependencies.count()
+ @cached_property
+ def license_detections_count(self):
+ """Return the number of license detections in this project."""
+ return self.discoveredlicenses.count()
+
@cached_property
def message_count(self):
"""Return the number of messages related to this project."""
@@ -3477,7 +3482,7 @@ def as_spdx(self):
class DiscoveredLicenseQuerySet(ProjectRelatedQuerySet):
def order_by_count_and_expression(self):
"""Order by detection count and license expression (identifer) fields."""
- return self.order_by("detection_count", "identifier")
+ return self.order_by("-detection_count", "identifier")
class AbstractLicenseDetection(models.Model):
diff --git a/scanpipe/templates/scanpipe/includes/project_summary_level.html b/scanpipe/templates/scanpipe/includes/project_summary_level.html
index 2dad4e586..2e46585a8 100644
--- a/scanpipe/templates/scanpipe/includes/project_summary_level.html
+++ b/scanpipe/templates/scanpipe/includes/project_summary_level.html
@@ -40,6 +40,20 @@
+
Resources
From d1b43c134935a05659e976634f3534c0095a1791 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Mon, 25 Mar 2024 21:13:06 +0530
Subject: [PATCH 04/12] Support LicenseDetection creation in all pipelines
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/models.py | 30 ++++++++++++
scanpipe/pipelines/deploy_to_develop.py | 8 ++++
scanpipe/pipelines/docker.py | 1 +
scanpipe/pipelines/docker_windows.py | 1 +
scanpipe/pipelines/root_filesystem.py | 8 ++++
scanpipe/pipelines/scan_codebase.py | 8 ++++
scanpipe/pipes/__init__.py | 22 +++++++--
scanpipe/pipes/scancode.py | 62 +++++++++++++++++++++++++
8 files changed, 137 insertions(+), 3 deletions(-)
diff --git a/scanpipe/models.py b/scanpipe/models.py
index 44d72fb46..c3ce73416 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -2726,6 +2726,16 @@ def with_resources_count(self):
)
return self.annotate(resources_count=count_subquery)
+ def has_license_detections(self):
+ return self.filter(
+ ~Q(license_detections=[]) | ~Q(other_license_detections=[])
+ )
+
+ def has_no_license_detections(self):
+ return self.filter(
+ Q(license_detections=[]) & Q(other_license_detections=[])
+ )
+
class AbstractPackage(models.Model):
"""These fields should be kept in line with `packagedcode.models.PackageData`."""
@@ -3506,6 +3516,7 @@ class AbstractLicenseDetection(models.Model):
)
matches = models.JSONField(
+ _("Reference Matches"),
default=list,
blank=True,
help_text=_('List of license matches combined in this detection.'),
@@ -3547,6 +3558,10 @@ class DiscoveredLicense(
"""
license_expression_field = "license_expression"
+ # If this license was discovered in a extracted license statement
+ # this is True, and False if this was discovered in a file.
+ from_package = None
+
detection_count = models.BigIntegerField(
blank=True,
null=True,
@@ -3554,6 +3569,7 @@ class DiscoveredLicense(
)
file_regions = models.JSONField(
+ _("Detection Locations"),
default=list,
blank=True,
help_text=_(
@@ -3622,6 +3638,20 @@ def create_from_data(cls, project, detection_data):
discovered_license.save(save_error=False, capture_exception=False)
return discovered_license
+ def update_with_file_region(self, file_region):
+ """
+ If the `file_region` is a new file region, include it in the
+ `file_regions` list and increase the `detection_count` by 1.
+ """
+ file_region_data = file_region.to_dict()
+ if not file_region_data in self.file_regions:
+ self.file_regions.append(file_region_data)
+ if not self.detection_count:
+ self.detection_count = 1
+ else:
+ self.detection_count += 1
+ self.save(update_fields=["detection_count", "file_regions"])
+
class WebhookSubscription(UUIDPKModel, ProjectRelatedModel):
target_url = models.URLField(_("Target URL"), max_length=1024)
diff --git a/scanpipe/pipelines/deploy_to_develop.py b/scanpipe/pipelines/deploy_to_develop.py
index 311495e22..f8add21b6 100644
--- a/scanpipe/pipelines/deploy_to_develop.py
+++ b/scanpipe/pipelines/deploy_to_develop.py
@@ -83,6 +83,7 @@ def steps(cls):
cls.remove_packages_without_resources,
cls.scan_unmapped_to_files,
cls.scan_mapped_from_for_files,
+ cls.collect_and_create_license_detections,
cls.flag_deployed_from_resources_with_missing_license,
cls.create_local_files_packages,
)
@@ -286,6 +287,13 @@ def scan_mapped_from_for_files(self):
scan_files = d2d.get_from_files_for_scanning(self.project.codebaseresources)
scancode.scan_for_files(self.project, scan_files, progress_logger=self.log)
+ def collect_and_create_license_detections(self):
+ """
+ Collect and create unique license detections from resources and
+ package data.
+ """
+ scancode.collect_and_create_license_detections(project=self.project)
+
def create_local_files_packages(self):
"""Create local-files packages for codebase resources not part of a package."""
d2d.create_local_files_packages(self.project)
diff --git a/scanpipe/pipelines/docker.py b/scanpipe/pipelines/docker.py
index 3e0897226..c0ab10662 100644
--- a/scanpipe/pipelines/docker.py
+++ b/scanpipe/pipelines/docker.py
@@ -42,6 +42,7 @@ def steps(cls):
cls.flag_ignored_resources,
cls.scan_for_application_packages,
cls.scan_for_files,
+ cls.collect_and_create_license_detections,
cls.analyze_scanned_files,
cls.flag_not_analyzed_codebase_resources,
)
diff --git a/scanpipe/pipelines/docker_windows.py b/scanpipe/pipelines/docker_windows.py
index 2698824fb..3f7513131 100644
--- a/scanpipe/pipelines/docker_windows.py
+++ b/scanpipe/pipelines/docker_windows.py
@@ -45,6 +45,7 @@ def steps(cls):
cls.flag_ignored_resources,
cls.scan_for_application_packages,
cls.scan_for_files,
+ cls.collect_and_create_license_detections,
cls.analyze_scanned_files,
cls.flag_data_files_with_no_clues,
cls.flag_not_analyzed_codebase_resources,
diff --git a/scanpipe/pipelines/root_filesystem.py b/scanpipe/pipelines/root_filesystem.py
index ebfae326e..a40190268 100644
--- a/scanpipe/pipelines/root_filesystem.py
+++ b/scanpipe/pipelines/root_filesystem.py
@@ -45,6 +45,7 @@ def steps(cls):
cls.scan_for_application_packages,
cls.match_not_analyzed_to_system_packages,
cls.scan_for_files,
+ cls.collect_and_create_license_detections,
cls.analyze_scanned_files,
cls.flag_not_analyzed_codebase_resources,
)
@@ -123,6 +124,13 @@ def scan_for_files(self):
"""Scan unknown resources for copyrights, licenses, emails, and urls."""
scancode.scan_for_files(self.project, progress_logger=self.log)
+ def collect_and_create_license_detections(self):
+ """
+ Collect and create unique license detections from resources and
+ package data.
+ """
+ scancode.collect_and_create_license_detections(project=self.project)
+
def analyze_scanned_files(self):
"""Analyze single file scan results for completeness."""
flag.analyze_scanned_files(self.project)
diff --git a/scanpipe/pipelines/scan_codebase.py b/scanpipe/pipelines/scan_codebase.py
index 668e71d6c..45605fd21 100644
--- a/scanpipe/pipelines/scan_codebase.py
+++ b/scanpipe/pipelines/scan_codebase.py
@@ -45,6 +45,7 @@ def steps(cls):
cls.flag_ignored_resources,
cls.scan_for_application_packages,
cls.scan_for_files,
+ cls.collect_and_create_license_detections,
)
def copy_inputs_to_codebase_directory(self):
@@ -65,3 +66,10 @@ def scan_for_application_packages(self):
def scan_for_files(self):
"""Scan unknown resources for copyrights, licenses, emails, and urls."""
scancode.scan_for_files(self.project, progress_logger=self.log)
+
+ def collect_and_create_license_detections(self):
+ """
+ Collect and create unique license detections from resources and
+ package data.
+ """
+ scancode.collect_and_create_license_detections(project=self.project)
diff --git a/scanpipe/pipes/__init__.py b/scanpipe/pipes/__init__.py
index be7c7aff8..3a1bea41e 100644
--- a/scanpipe/pipes/__init__.py
+++ b/scanpipe/pipes/__init__.py
@@ -246,11 +246,18 @@ def update_or_create_dependency(
return dependency
-def update_or_create_license_detection(project, detection_data):
+def update_or_create_license_detection(
+ project, detection_data, resource_path, from_package=False,
+):
"""
Get, update or create a DiscoveredLicense object then return it.
Use the `project` and `detection_data` mapping to lookup and creates the
DiscoveredLicense using its detection identifier as a unique key.
+
+ Additonally if `resource_path` is passed, add the file region where
+ the license was detected to the DiscoveredLicense object, if not present
+ already. `from_package` is True if the license detection was in a
+ `extracted_license_statement` from a package metadata.
"""
detection_identifier = detection_data["identifier"]
@@ -267,13 +274,22 @@ def update_or_create_license_detection(project, detection_data):
detection_data,
)
+ if resource_path:
+ file_region = scancode.get_file_region(
+ detection_data=detection_data,
+ resource_path=resource_path,
+ )
+ license_detection.update_with_file_region(file_region)
+
+ license_detection.from_package = from_package
return license_detection
def _clean_license_detection_data(detection_data):
detection_data = detection_data.copy()
- matches = detection_data.pop("sample_matches")
- detection_data["matches"] = matches
+ if "sample_matches" in detection_data:
+ matches = detection_data.pop("sample_matches")
+ detection_data["matches"] = matches
return detection_data
diff --git a/scanpipe/pipes/scancode.py b/scanpipe/pipes/scancode.py
index a1174a221..aadc4a62c 100644
--- a/scanpipe/pipes/scancode.py
+++ b/scanpipe/pipes/scancode.py
@@ -39,6 +39,7 @@
from extractcode import api as extractcode_api
from packagedcode import get_package_handler
from packagedcode import models as packagedcode_models
+from licensedcode.detection import FileRegion
from scancode import Scanner
from scancode import api as scancode_api
from scancode import cli as scancode_cli
@@ -411,6 +412,67 @@ def add_resource_to_package(package_uid, resource, project):
resource.discovered_packages.add(package)
+def collect_and_create_license_detections(project):
+ """
+ Create instances of DiscoveredLicense for `project` from the parsed
+ license detections present in the CodebaseResources and
+ DiscoveredPackages of `project`.
+ """
+ logger.info(f"Project {project} collect_license_detections:")
+
+ for resource in project.codebaseresources.has_license_detections():
+ logger.info(f" Processing: {resource.path} for licenses")
+
+ for detection_data in resource.license_detections:
+ pipes.update_or_create_license_detection(
+ project=project,
+ detection_data=detection_data,
+ resource_path=resource.path,
+ )
+
+ for resource in project.codebaseresources.has_package_data():
+
+ for package_mapping in resource.package_data:
+ package_data = packagedcode_models.PackageData.from_dict(
+ mapping=package_mapping,
+ )
+
+ for detection in package_data.license_detections:
+ pipes.update_or_create_license_detection(
+ project=project,
+ detection_data=detection,
+ resource_path=resource.path,
+ from_package=True,
+ )
+
+ for detection in package_data.other_license_detections:
+ pipes.update_or_create_license_detection(
+ project=project,
+ detection_data=detection,
+ resource_path=resource.path,
+ from_package=True,
+ )
+
+
+def get_file_region(detection_data, resource_path):
+ """
+ From a LicenseDetection mapping `detection_data`, create a FileRegion
+ object containing information about where this license was detected
+ exactly in a codebase, with `resource_path`, with start and end lines.
+ """
+ start_line = min(
+ [match['start_line'] for match in detection_data["matches"]]
+ )
+ end_line = max(
+ [match['end_line'] for match in detection_data["matches"]]
+ )
+ return FileRegion(
+ path=resource_path,
+ start_line=start_line,
+ end_line=end_line,
+ )
+
+
def assemble_packages(project):
"""
Create instances of DiscoveredPackage and DiscoveredDependency for `project`
From 96268abf4e05bf80ab79fba4b4ad741200ea2d26 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Mon, 25 Mar 2024 21:13:32 +0530
Subject: [PATCH 05/12] Fix linter checks
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/api/serializers.py | 2 +-
scanpipe/filters.py | 2 +-
scanpipe/models.py | 56 +++++++++++++++++++------------------
scanpipe/pipes/__init__.py | 7 +++--
scanpipe/pipes/input.py | 2 +-
scanpipe/pipes/scancode.py | 12 +++-----
scanpipe/views.py | 6 ++--
7 files changed, 44 insertions(+), 43 deletions(-)
diff --git a/scanpipe/api/serializers.py b/scanpipe/api/serializers.py
index f1e9ad88c..4de2c4b09 100644
--- a/scanpipe/api/serializers.py
+++ b/scanpipe/api/serializers.py
@@ -30,8 +30,8 @@
from scanpipe.models import CodebaseRelation
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
-from scanpipe.models import DiscoveredPackage
from scanpipe.models import DiscoveredLicense
+from scanpipe.models import DiscoveredPackage
from scanpipe.models import InputSource
from scanpipe.models import Project
from scanpipe.models import ProjectMessage
diff --git a/scanpipe/filters.py b/scanpipe/filters.py
index 92ef8e635..57eada085 100644
--- a/scanpipe/filters.py
+++ b/scanpipe/filters.py
@@ -38,8 +38,8 @@
from scanpipe.models import CodebaseRelation
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
-from scanpipe.models import DiscoveredPackage
from scanpipe.models import DiscoveredLicense
+from scanpipe.models import DiscoveredPackage
from scanpipe.models import Project
from scanpipe.models import ProjectMessage
from scanpipe.models import Run
diff --git a/scanpipe/models.py b/scanpipe/models.py
index c3ce73416..52759901d 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -2727,14 +2727,10 @@ def with_resources_count(self):
return self.annotate(resources_count=count_subquery)
def has_license_detections(self):
- return self.filter(
- ~Q(license_detections=[]) | ~Q(other_license_detections=[])
- )
+ return self.filter(~Q(license_detections=[]) | ~Q(other_license_detections=[]))
def has_no_license_detections(self):
- return self.filter(
- Q(license_detections=[]) & Q(other_license_detections=[])
- )
+ return self.filter(Q(license_detections=[]) & Q(other_license_detections=[]))
class AbstractPackage(models.Model):
@@ -3504,30 +3500,30 @@ class AbstractLicenseDetection(models.Model):
license_expression = models.TextField(
blank=True,
help_text=_(
- 'A license expression string using the SPDX license expression'
- ' syntax and ScanCode license keys, the effective license expression'
- ' for this license detection.'
+ "A license expression string using the SPDX license expression"
+ " syntax and ScanCode license keys, the effective license expression"
+ " for this license detection."
),
)
license_expression_spdx = models.TextField(
blank=True,
- help_text=_('SPDX license expression string with SPDX ids.'),
+ help_text=_("SPDX license expression string with SPDX ids."),
)
matches = models.JSONField(
_("Reference Matches"),
default=list,
blank=True,
- help_text=_('List of license matches combined in this detection.'),
+ help_text=_("List of license matches combined in this detection."),
)
detection_log = models.JSONField(
default=list,
blank=True,
help_text=_(
- 'A list of detection DetectionRule explaining how '
- 'this detection was created.'
+ "A list of detection DetectionRule explaining how "
+ "this detection was created."
),
)
@@ -3535,8 +3531,8 @@ class AbstractLicenseDetection(models.Model):
max_length=1024,
blank=True,
help_text=_(
- 'An identifier unique for a license detection, containing the license '
- 'expression and a UUID crafted from the match contents.'
+ "An identifier unique for a license detection, containing the license "
+ "expression and a UUID crafted from the match contents."
),
)
@@ -3552,10 +3548,11 @@ class DiscoveredLicense(
AbstractLicenseDetection,
):
"""
- A project's Discovered Licenses are the unique License Detection objects
+ A project's Discovered Licenses are the unique License Detection objects
discovered in the code under analysis.
"""
+
license_expression_field = "license_expression"
# If this license was discovered in a extracted license statement
@@ -3573,10 +3570,10 @@ class DiscoveredLicense(
default=list,
blank=True,
help_text=_(
- 'A list of file regions with resource path, start and end line '
- 'details for each place this license detection was discovered at. '
- 'Also contains whether this license was discovered from a file or '
- 'from package metadata.'
+ "A list of file regions with resource path, start and end line "
+ "details for each place this license detection was discovered at. "
+ "Also contains whether this license was discovered from a file or "
+ "from package metadata."
),
)
@@ -3604,9 +3601,10 @@ def __str__(self):
@classmethod
def create_from_data(cls, project, detection_data):
"""
- Create and returns a DiscoveredLicense for a `project` from the `detection_data`.
- If one of the values of the required fields is not available, a "ProjectMessage"
- is created instead of a new DiscoveredLicense instance.
+ Create and returns a DiscoveredLicense for a `project` from the
+ `detection_data`. If one of the values of the required fields is not
+ available, a "ProjectMessage" is created instead of a new
+ DiscoveredLicense instance.
"""
detection_data = detection_data.copy()
required_fields = ["license_expression", "identifier", "matches"]
@@ -3622,7 +3620,11 @@ def create_from_data(cls, project, detection_data):
f"{', '.join(missing_values)}"
)
- project.add_warning(description=message, model=cls, details=detection_data)
+ project.add_warning(
+ description=message,
+ model=cls,
+ details=detection_data,
+ )
return
cleaned_data = {
@@ -3633,8 +3635,8 @@ def create_from_data(cls, project, detection_data):
discovered_license = cls(project=project, **cleaned_data)
# Using save_error=False to not capture potential errors at this level but
- # rather in the CodebaseResource.create_and_add_license_data method so resource data
- # can be injected in the ProjectMessage record.
+ # rather in the CodebaseResource.create_and_add_license_data method so
+ # resource data can be injected in the ProjectMessage record.
discovered_license.save(save_error=False, capture_exception=False)
return discovered_license
@@ -3644,7 +3646,7 @@ def update_with_file_region(self, file_region):
`file_regions` list and increase the `detection_count` by 1.
"""
file_region_data = file_region.to_dict()
- if not file_region_data in self.file_regions:
+ if file_region_data not in self.file_regions:
self.file_regions.append(file_region_data)
if not self.detection_count:
self.detection_count = 1
diff --git a/scanpipe/pipes/__init__.py b/scanpipe/pipes/__init__.py
index 3a1bea41e..96d2bf7b3 100644
--- a/scanpipe/pipes/__init__.py
+++ b/scanpipe/pipes/__init__.py
@@ -36,8 +36,8 @@
from scanpipe.models import CodebaseRelation
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
-from scanpipe.models import DiscoveredPackage
from scanpipe.models import DiscoveredLicense
+from scanpipe.models import DiscoveredPackage
from scanpipe.pipes import scancode
logger = logging.getLogger("scanpipe.pipes")
@@ -247,7 +247,10 @@ def update_or_create_dependency(
def update_or_create_license_detection(
- project, detection_data, resource_path, from_package=False,
+ project,
+ detection_data,
+ resource_path,
+ from_package=False,
):
"""
Get, update or create a DiscoveredLicense object then return it.
diff --git a/scanpipe/pipes/input.py b/scanpipe/pipes/input.py
index 41e5eb16a..3035ef907 100644
--- a/scanpipe/pipes/input.py
+++ b/scanpipe/pipes/input.py
@@ -34,8 +34,8 @@
from scanpipe.models import CodebaseRelation
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
-from scanpipe.models import DiscoveredPackage
from scanpipe.models import DiscoveredLicense
+from scanpipe.models import DiscoveredPackage
from scanpipe.pipes import scancode
from scanpipe.pipes.output import mappings_key_by_fieldname
diff --git a/scanpipe/pipes/scancode.py b/scanpipe/pipes/scancode.py
index aadc4a62c..327d3c6d1 100644
--- a/scanpipe/pipes/scancode.py
+++ b/scanpipe/pipes/scancode.py
@@ -37,9 +37,9 @@
from commoncode import fileutils
from commoncode.resource import VirtualCodebase
from extractcode import api as extractcode_api
+from licensedcode.detection import FileRegion
from packagedcode import get_package_handler
from packagedcode import models as packagedcode_models
-from licensedcode.detection import FileRegion
from scancode import Scanner
from scancode import api as scancode_api
from scancode import cli as scancode_cli
@@ -460,12 +460,8 @@ def get_file_region(detection_data, resource_path):
object containing information about where this license was detected
exactly in a codebase, with `resource_path`, with start and end lines.
"""
- start_line = min(
- [match['start_line'] for match in detection_data["matches"]]
- )
- end_line = max(
- [match['end_line'] for match in detection_data["matches"]]
- )
+ start_line = min([match["start_line"] for match in detection_data["matches"]])
+ end_line = max([match["end_line"] for match in detection_data["matches"]])
return FileRegion(
path=resource_path,
start_line=start_line,
@@ -694,7 +690,7 @@ def create_discovered_licenses(project, scanned_codebase):
"""
Save the license detections of a ScanCode `scanned_codebase`
scancode.resource.Codebase object to the database as a DiscoveredLicense of
- `project`.
+ `project`.
"""
if hasattr(scanned_codebase.attributes, "license_detections"):
for detection_data in scanned_codebase.attributes.license_detections:
diff --git a/scanpipe/views.py b/scanpipe/views.py
index ec0afe5fb..dc2badd41 100644
--- a/scanpipe/views.py
+++ b/scanpipe/views.py
@@ -64,8 +64,8 @@
from scanpipe.api.serializers import DiscoveredDependencySerializer
from scanpipe.filters import PAGE_VAR
from scanpipe.filters import DependencyFilterSet
-from scanpipe.filters import PackageFilterSet
from scanpipe.filters import LicenseFilterSet
+from scanpipe.filters import PackageFilterSet
from scanpipe.filters import ProjectFilterSet
from scanpipe.filters import ProjectMessageFilterSet
from scanpipe.filters import RelationFilterSet
@@ -82,8 +82,8 @@
from scanpipe.models import CodebaseRelation
from scanpipe.models import CodebaseResource
from scanpipe.models import DiscoveredDependency
-from scanpipe.models import DiscoveredPackage
from scanpipe.models import DiscoveredLicense
+from scanpipe.models import DiscoveredPackage
from scanpipe.models import InputSource
from scanpipe.models import Project
from scanpipe.models import ProjectMessage
@@ -2009,7 +2009,7 @@ def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context["dependency_data"] = DiscoveredDependencySerializer(self.object).data
return context
-
+
class DiscoveredLicenseDetailsView(
ConditionalLoginRequired,
From a025a5e307fafbe7390f4ffb8e6e31abb3d72e02 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Mon, 25 Mar 2024 21:19:56 +0530
Subject: [PATCH 06/12] Reposition migrations after rebasing main
Signed-off-by: Ayan Sinha Mahapatra
---
...ered_license_models.py => 0056_discovered_license_models.py} | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
rename scanpipe/migrations/{0054_discovered_license_models.py => 0056_discovered_license_models.py} (98%)
diff --git a/scanpipe/migrations/0054_discovered_license_models.py b/scanpipe/migrations/0056_discovered_license_models.py
similarity index 98%
rename from scanpipe/migrations/0054_discovered_license_models.py
rename to scanpipe/migrations/0056_discovered_license_models.py
index d4253eb6f..61511d46b 100644
--- a/scanpipe/migrations/0054_discovered_license_models.py
+++ b/scanpipe/migrations/0056_discovered_license_models.py
@@ -8,7 +8,7 @@
class Migration(migrations.Migration):
dependencies = [
- ("scanpipe", "0053_restructure_pipelines_data"),
+ ("scanpipe", "0055_discoveredpackage_datafile_paths"),
]
operations = [
From b20ce50416b4285e1ff1987bfcb6061862063788 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Thu, 28 Mar 2024 02:13:42 +0530
Subject: [PATCH 07/12] Fix SCIO tests
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/pipes/__init__.py | 14 +++++++++++---
scanpipe/tests/test_models.py | 1 +
scanpipe/tests/test_views.py | 2 +-
3 files changed, 13 insertions(+), 4 deletions(-)
diff --git a/scanpipe/pipes/__init__.py b/scanpipe/pipes/__init__.py
index 96d2bf7b3..a4bd722f3 100644
--- a/scanpipe/pipes/__init__.py
+++ b/scanpipe/pipes/__init__.py
@@ -249,7 +249,7 @@ def update_or_create_dependency(
def update_or_create_license_detection(
project,
detection_data,
- resource_path,
+ resource_path=None,
from_package=False,
):
"""
@@ -277,6 +277,14 @@ def update_or_create_license_detection(
detection_data,
)
+ if not license_detection:
+ project.add_error(
+ model="update_or_create_license_detection",
+ details=detection_data,
+ resource=resource_path,
+ )
+ return
+
if resource_path:
file_region = scancode.get_file_region(
detection_data=detection_data,
@@ -290,8 +298,8 @@ def update_or_create_license_detection(
def _clean_license_detection_data(detection_data):
detection_data = detection_data.copy()
- if "sample_matches" in detection_data:
- matches = detection_data.pop("sample_matches")
+ if "reference_matches" in detection_data:
+ matches = detection_data.pop("reference_matches")
detection_data["matches"] = matches
return detection_data
diff --git a/scanpipe/tests/test_models.py b/scanpipe/tests/test_models.py
index 50cb75120..7fc71cb52 100644
--- a/scanpipe/tests/test_models.py
+++ b/scanpipe/tests/test_models.py
@@ -168,6 +168,7 @@ def test_scanpipe_project_model_delete_related_objects(self):
"scanpipe.CodebaseRelation": 0,
"scanpipe.CodebaseResource": 1,
"scanpipe.DiscoveredDependency": 0,
+ "scanpipe.DiscoveredLicense": 0,
"scanpipe.DiscoveredPackage": 1,
"scanpipe.DiscoveredPackage_codebase_resources": 1,
"scanpipe.ProjectMessage": 0,
diff --git a/scanpipe/tests/test_views.py b/scanpipe/tests/test_views.py
index 554a812b7..491f816ed 100644
--- a/scanpipe/tests/test_views.py
+++ b/scanpipe/tests/test_views.py
@@ -683,7 +683,7 @@ def test_scanpipe_views_project_views(self):
with self.assertNumQueries(8):
self.client.get(url)
- with self.assertNumQueries(13):
+ with self.assertNumQueries(14):
self.client.get(self.project1.get_absolute_url())
@mock.patch("scanpipe.models.Run.execute_task_async")
From 3cb1a2cc52cc2d2a77d17ca56271cc6896a4334e Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Thu, 28 Mar 2024 16:43:50 +0530
Subject: [PATCH 08/12] Improve detail views with links in UI
This commit improves license, package and resource details
views with detection tabs and links to each other.
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/models.py | 12 ++-
scanpipe/pipes/__init__.py | 16 ++-
scanpipe/pipes/scancode.py | 34 +++++-
.../panels/resource_license_summary.html | 2 +-
.../scanpipe/tabset/tab_detections.html | 34 ------
.../tabset/tab_license_detections.html | 88 +++++++++++++++
.../tabset/tab_package_detections.html | 87 +++++++++++++++
.../tabset/tab_resource_detections.html | 102 ++++++++++++++++++
scanpipe/tests/test_views.py | 12 +--
scanpipe/views.py | 37 ++++---
10 files changed, 357 insertions(+), 67 deletions(-)
delete mode 100644 scanpipe/templates/scanpipe/tabset/tab_detections.html
create mode 100644 scanpipe/templates/scanpipe/tabset/tab_license_detections.html
create mode 100644 scanpipe/templates/scanpipe/tabset/tab_package_detections.html
create mode 100644 scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
diff --git a/scanpipe/models.py b/scanpipe/models.py
index 52759901d..c7d0bf513 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -3640,7 +3640,7 @@ def create_from_data(cls, project, detection_data):
discovered_license.save(save_error=False, capture_exception=False)
return discovered_license
- def update_with_file_region(self, file_region):
+ def update_with_file_region(self, file_region, count_detection):
"""
If the `file_region` is a new file region, include it in the
`file_regions` list and increase the `detection_count` by 1.
@@ -3648,10 +3648,12 @@ def update_with_file_region(self, file_region):
file_region_data = file_region.to_dict()
if file_region_data not in self.file_regions:
self.file_regions.append(file_region_data)
- if not self.detection_count:
- self.detection_count = 1
- else:
- self.detection_count += 1
+ if count_detection:
+ if not self.detection_count:
+ self.detection_count = 1
+ else:
+ self.detection_count += 1
+
self.save(update_fields=["detection_count", "file_regions"])
diff --git a/scanpipe/pipes/__init__.py b/scanpipe/pipes/__init__.py
index a4bd722f3..a48a818b8 100644
--- a/scanpipe/pipes/__init__.py
+++ b/scanpipe/pipes/__init__.py
@@ -251,6 +251,7 @@ def update_or_create_license_detection(
detection_data,
resource_path=None,
from_package=False,
+ count_detection=True,
):
"""
Get, update or create a DiscoveredLicense object then return it.
@@ -290,7 +291,10 @@ def update_or_create_license_detection(
detection_data=detection_data,
resource_path=resource_path,
)
- license_detection.update_with_file_region(file_region)
+ license_detection.update_with_file_region(
+ file_region=file_region,
+ count_detection=count_detection,
+ )
license_detection.from_package = from_package
return license_detection
@@ -301,6 +305,16 @@ def _clean_license_detection_data(detection_data):
if "reference_matches" in detection_data:
matches = detection_data.pop("reference_matches")
detection_data["matches"] = matches
+
+ updated_matches = []
+ for match_data in detection_data["matches"]:
+ from_file_path = match_data["from_file"]
+ if from_file_path:
+ match_data["from_file"] = from_file_path.removeprefix("codebase/")
+
+ updated_matches.append(match_data)
+
+ detection_data["matches"] = updated_matches
return detection_data
diff --git a/scanpipe/pipes/scancode.py b/scanpipe/pipes/scancode.py
index 327d3c6d1..fc818a403 100644
--- a/scanpipe/pipes/scancode.py
+++ b/scanpipe/pipes/scancode.py
@@ -647,10 +647,28 @@ def create_codebase_resources(project, scanned_codebase):
license_detections = getattr(scanned_resource, "license_detections", [])
for detection_data in license_detections:
detection_identifier = detection_data.get("identifier")
- license_detection = project.discoveredlicenses.get_or_none(
- identifier=detection_identifier
+ pipes.update_or_create_license_detection(
+ project=project,
+ detection_data=detection_data,
+ resource_path=resource_path,
+ count_detection=False,
)
- logger.debug(f"Add {codebase_resource} to {license_detection}")
+ logger.debug(f"Add {codebase_resource} to {detection_identifier}")
+
+ packages = getattr(scanned_resource, "package_data", [])
+ for package_data in packages:
+ license_detections = package_data.get("license_detections", [])
+ license_detections.extend(package_data.get("other_license_detections", []))
+ for detection_data in license_detections:
+ detection_identifier = detection_data.get("identifier")
+ pipes.update_or_create_license_detection(
+ project=project,
+ detection_data=detection_data,
+ resource_path=resource_path,
+ count_detection=False,
+ from_package=True,
+ )
+ logger.debug(f"Add {codebase_resource} to {detection_identifier}")
def create_discovered_packages(project, scanned_codebase):
@@ -661,6 +679,16 @@ def create_discovered_packages(project, scanned_codebase):
if hasattr(scanned_codebase.attributes, "packages"):
for package_data in scanned_codebase.attributes.packages:
pipes.update_or_create_package(project, package_data)
+ license_detections = package_data.get("license_detections", [])
+ license_detections.extend(package_data.get("other_license_detections", []))
+
+ for license_detection in license_detections:
+ pipes.update_or_create_license_detection(
+ project=project,
+ detection_data=license_detection,
+ from_package=True,
+ count_detection=False,
+ )
def create_discovered_dependencies(
diff --git a/scanpipe/templates/scanpipe/panels/resource_license_summary.html b/scanpipe/templates/scanpipe/panels/resource_license_summary.html
index 7a0f72388..c2e158753 100644
--- a/scanpipe/templates/scanpipe/panels/resource_license_summary.html
+++ b/scanpipe/templates/scanpipe/panels/resource_license_summary.html
@@ -3,7 +3,7 @@
- Top 10 detected licenses in files
+ Top 10 detected license expressions for files
{% for license_expression, count in resource_license_summary.items %}
diff --git a/scanpipe/templates/scanpipe/tabset/tab_detections.html b/scanpipe/templates/scanpipe/tabset/tab_detections.html
deleted file mode 100644
index ab85c2ab3..000000000
--- a/scanpipe/templates/scanpipe/tabset/tab_detections.html
+++ /dev/null
@@ -1,34 +0,0 @@
-
-
-
-
- Datafile Paths
-
-
-
- {% for path in tab_data.fields.datafile_paths.value %}
-
-
- {{ path }}
-
-
- {% endfor %}
-
-
-
-
-
- Datasource IDs
-
-
-
- {% for id in tab_data.fields.datasource_ids.value %}
-
-
- {{ id }}
-
-
- {% endfor %}
-
-
-
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/tabset/tab_license_detections.html b/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
new file mode 100644
index 000000000..ce6eba017
--- /dev/null
+++ b/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
@@ -0,0 +1,88 @@
+
+
+
+
+ License Expression
+ Origin Resource Path
+ Rule URL
+ Score
+ Matcher
+ Match Length
+ Match Coverage
+ Rule Relevance
+
+
+
+ {% for match in tab_data.fields.matches.value %}
+
+
+ {{ match.license_expression }}
+
+
+ {{ match.from_file }}
+
+
+ {{ match.rule_url }}
+
+
+ {{ match.score }}
+
+
+ {{ match.matcher }}
+
+
+ {{ match.matched_length }}
+
+
+ {{ match.match_coverage }}
+
+
+ {{ match.rule_relevance }}
+
+
+ {% endfor %}
+
+
+ {% if tab_data.fields.detection_log.value %}
+
+
+
+ Detection Log
+
+
+
+ {% for log_entry in tab_data.fields.detection_log.value %}
+
+
+ {{ log_entry }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+
+
+
+ Resource Path
+ Start Line
+ End Line
+
+
+
+ {% for file_region in tab_data.fields.file_regions.value %}
+
+
+ {{ file_region.path }}
+
+
+ {{ file_region.start_line }}
+
+
+ {{ file_region.end_line }}
+
+
+ {% endfor %}
+
+
+
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/tabset/tab_package_detections.html b/scanpipe/templates/scanpipe/tabset/tab_package_detections.html
new file mode 100644
index 000000000..af09167b8
--- /dev/null
+++ b/scanpipe/templates/scanpipe/tabset/tab_package_detections.html
@@ -0,0 +1,87 @@
+
+
+
+
+ Datafile Paths
+
+
+
+ {% for path in tab_data.fields.datafile_paths.value %}
+
+
+ {{ path }}
+
+
+ {% endfor %}
+
+
+
+
+
+ Datasource IDs
+
+
+
+ {% for id in tab_data.fields.datasource_ids.value %}
+
+
+ {{ id }}
+
+
+ {% endfor %}
+
+
+ {% if tab_data.fields.license_detections.value %}
+
+
+
+ License Detections
+ License Expression
+ License Expression SPDX
+
+
+
+ {% for detection in tab_data.fields.license_detections.value %}
+
+
+ {{ detection.identifier }}
+
+
+ {{ detection.license_expression }}
+
+
+ {{ detection.license_expression_spdx }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% if tab_data.fields.other_license_detections.value %}
+
+
+
+ License Detections
+ License Expression
+ License Expression SPDX
+
+
+
+ {% for detection in tab_data.fields.other_license_detections.value %}
+
+
+ {{ detection.identifier }}
+
+
+
+ {{ detection.license_expression }}
+
+
+ {{ detection.license_expression_spdx }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html b/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
new file mode 100644
index 000000000..4cd3c37ed
--- /dev/null
+++ b/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
@@ -0,0 +1,102 @@
+
+ {% if tab_data.fields.license_detections.value %}
+
+
+
+ License Detections
+ License Expression
+ License Expression SPDX
+
+
+
+ {% for detection in tab_data.fields.license_detections.value %}
+
+
+ {{ detection.identifier }}
+
+
+ {{ detection.license_expression }}
+
+
+ {{ detection.license_expression_spdx }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% if tab_data.fields.license_clues.value %}
+
+
+
+ License Expression
+ License Clue Detials
+
+
+
+ {% for clue in tab_data.fields.license_clues.value %}
+
+
+ {{ clue.license_expression }}
+
+
+ {{ clue }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% if tab_data.fields.emails.value %}
+
+
+
+ Email
+ Start Line
+ End Line
+
+
+
+ {% for email in tab_data.fields.emails.value %}
+
+
+ {{ email.email }}
+
+
+ {{ email.start_line }}
+
+
+ {{ email.end_line }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+ {% if tab_data.fields.urls.value %}
+
+
+
+ URL
+ Start Line
+ End Line
+
+
+
+ {% for url in tab_data.fields.urls.value %}
+
+
+ {{ url.url }}
+
+
+ {{ url.start_line }}
+
+
+ {{ url.end_line }}
+
+
+ {% endfor %}
+
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/scanpipe/tests/test_views.py b/scanpipe/tests/test_views.py
index 491f816ed..7130bd3d5 100644
--- a/scanpipe/tests/test_views.py
+++ b/scanpipe/tests/test_views.py
@@ -825,8 +825,10 @@ def test_scanpipe_views_codebase_resource_details_view_tabset(self):
self.assertContains(response, 'id="tab-others"')
self.assertContains(response, 'data-target="tab-viewer"')
self.assertContains(response, 'id="tab-viewer"')
- self.assertNotContains(response, 'data-target="tab-detection"')
- self.assertNotContains(response, 'id="tab-detection"')
+ self.assertNotContains(response, 'data-target="tab-terms"')
+ self.assertNotContains(response, 'id="tab-terms"')
+ self.assertNotContains(response, 'data-target="tab-resource-detection"')
+ self.assertNotContains(response, 'id="tab-resource-detection"')
self.assertNotContains(response, 'data-target="tab-packages"')
self.assertNotContains(response, 'id="tab-packages"')
self.assertNotContains(response, 'data-target="tab-relations"')
@@ -844,10 +846,8 @@ def test_scanpipe_views_codebase_resource_details_view_tabset(self):
map_type="path",
)
response = self.client.get(resource1.get_absolute_url())
- self.assertContains(response, 'data-target="tab-detection"')
- self.assertContains(response, 'id="tab-detection"')
- self.assertContains(response, 'data-target="tab-packages"')
- self.assertContains(response, 'id="tab-packages"')
+ self.assertContains(response, 'data-target="tab-terms"')
+ self.assertContains(response, 'id="tab-terms"')
self.assertContains(response, 'data-target="tab-relations"')
self.assertContains(response, 'id="tab-relations"')
self.assertContains(response, 'data-target="tab-extra_data"')
diff --git a/scanpipe/views.py b/scanpipe/views.py
index dc2badd41..11242acd7 100644
--- a/scanpipe/views.py
+++ b/scanpipe/views.py
@@ -311,7 +311,15 @@ def get_field_value(self, field_name, render_func=None):
if isinstance(field_value, Manager):
return list(field_value.all())
- list_fields = ["datafile_paths", "datasource_ids"]
+ list_fields = [
+ "license_detections",
+ "other_license_detections",
+ "license_clues",
+ "urls",
+ "emails",
+ "datafile_paths",
+ "datasource_ids",
+ ]
if isinstance(field_value, list):
if field_name not in list_fields:
@@ -1701,23 +1709,24 @@ class CodebaseResourceDetailsView(
"disable_condition": do_not_disable,
"display_condition": is_displayable_image_type,
},
- "detection": {
+ "terms": {
"fields": [
"detected_license_expression",
{
"field_name": "detected_license_expression_spdx",
"label": "Detected license expression (SPDX)",
},
- {"field_name": "license_detections", "render_func": render_as_yaml},
- {"field_name": "license_clues", "render_func": render_as_yaml},
"percentage_of_license_text",
{"field_name": "copyrights", "render_func": render_as_yaml},
{"field_name": "holders", "render_func": render_as_yaml},
{"field_name": "authors", "render_func": render_as_yaml},
- {"field_name": "emails", "render_func": render_as_yaml},
- {"field_name": "urls", "render_func": render_as_yaml},
],
+ "icon_class": "fa-solid fa-file-contract",
+ },
+ "detection": {
+ "fields": ["license_detections", "license_clues", "emails", "urls"],
"icon_class": "fa-solid fa-search",
+ "template": "scanpipe/tabset/tab_resource_detections.html",
},
"packages": {
"fields": ["discovered_packages"],
@@ -1904,11 +1913,6 @@ class DiscoveredPackageDetailsView(
"copyright",
"holder",
"notice_text",
- {"field_name": "license_detections", "render_func": render_as_yaml},
- {
- "field_name": "other_license_detections",
- "render_func": render_as_yaml,
- },
],
"icon_class": "fa-solid fa-file-contract",
},
@@ -1916,9 +1920,11 @@ class DiscoveredPackageDetailsView(
"fields": [
"datasource_ids",
"datafile_paths",
+ "license_detections",
+ "other_license_detections",
],
"icon_class": "fa-solid fa-search",
- "template": "scanpipe/tabset/tab_detections.html",
+ "template": "scanpipe/tabset/tab_package_detections.html",
},
"resources": {
"fields": ["codebase_resources"],
@@ -2033,12 +2039,9 @@ class DiscoveredLicenseDetailsView(
"icon_class": "fa-solid fa-info-circle",
},
"detection": {
- "fields": [
- {"field_name": "matches", "render_func": render_as_yaml},
- {"field_name": "detection_log", "render_func": render_as_yaml},
- {"field_name": "file_regions", "render_func": render_as_yaml},
- ],
+ "fields": ["matches", "detection_log", "file_regions"],
"icon_class": "fa-solid fa-search",
+ "template": "scanpipe/tabset/tab_license_detections.html",
},
}
From 2f7943f80b382d8d61bdf3de87dacfe7e0afd826 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Tue, 16 Jul 2024 19:52:31 +0530
Subject: [PATCH 09/12] Improve LicenseDetections UI
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/filters.py | 3 ++
scanpipe/models.py | 37 +++++++++++++--
.../includes/project_summary_level.html | 20 +++------
.../scanpipe/license_detection_list.html | 15 ++++---
scanpipe/templates/scanpipe/package_list.html | 5 +++
.../panels/license_detections_summary.html | 19 ++++++++
.../panels/resource_license_summary.html | 16 -------
.../templates/scanpipe/project_detail.html | 2 +-
.../scanpipe/tabset/tab_packages.html | 5 +++
scanpipe/tests/test_views.py | 4 +-
scanpipe/urls.py | 6 +--
scanpipe/views.py | 45 ++++++++++++-------
12 files changed, 117 insertions(+), 60 deletions(-)
create mode 100644 scanpipe/templates/scanpipe/panels/license_detections_summary.html
delete mode 100644 scanpipe/templates/scanpipe/panels/resource_license_summary.html
diff --git a/scanpipe/filters.py b/scanpipe/filters.py
index 57eada085..74e088df1 100644
--- a/scanpipe/filters.py
+++ b/scanpipe/filters.py
@@ -772,6 +772,8 @@ class Meta:
class LicenseFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
dropdown_widget_fields = [
"compliance_alert",
+ "license_expression",
+ "license_expression_spdx",
]
search = DiscoveredLicenseSearchFilter(
@@ -788,6 +790,7 @@ class LicenseFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
],
)
license_expression = ParentAllValuesFilter()
+ license_expression_spdx = ParentAllValuesFilter()
compliance_alert = django_filters.ChoiceFilter(
choices=[(EMPTY_VAR, "None")] + CodebaseResource.Compliance.choices,
)
diff --git a/scanpipe/models.py b/scanpipe/models.py
index c7d0bf513..468e4b6ca 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -1234,6 +1234,16 @@ def license_detections_count(self):
"""Return the number of license detections in this project."""
return self.discoveredlicenses.count()
+ @cached_property
+ def package_compliance_alert_count(self):
+ """Return the number of packages related to this project which have."""
+ return self.discoveredpackages.has_compliance_alert().count()
+
+ @cached_property
+ def license_compliance_alert_count(self):
+ """Return the number of packages related to this project which have."""
+ return self.discoveredlicenses.has_compliance_alert().count()
+
@cached_property
def message_count(self):
"""Return the number of messages related to this project."""
@@ -2156,6 +2166,17 @@ class Compliance(models.TextChoices):
class Meta:
abstract = True
+ @property
+ def has_compliance_alert(self):
+ """
+ Returns True if this instance has a compliance alert of `ERROR`
+ for it's respective license_expression fields.
+ """
+ if self.compliance_alert == self.Compliance.ERROR:
+ return True
+
+ return False
+
@classmethod
def from_db(cls, db, field_names, values):
"""
@@ -2710,8 +2731,16 @@ def vulnerable(self):
return self.filter(~Q(affected_by_vulnerabilities__in=EMPTY_VALUES))
+class ComplianceAlertQuerySetMixin:
+ def has_compliance_alert(self):
+ return self.filter(Q(compliance_alert__exact=CodebaseResource.Compliance.ERROR))
+
+
class DiscoveredPackageQuerySet(
- VulnerabilityQuerySetMixin, PackageURLQuerySetMixin, ProjectRelatedQuerySet
+ VulnerabilityQuerySetMixin,
+ ComplianceAlertQuerySetMixin,
+ PackageURLQuerySetMixin,
+ ProjectRelatedQuerySet,
):
def order_by_purl(self):
"""Order by Package URL fields."""
@@ -3485,7 +3514,10 @@ def as_spdx(self):
)
-class DiscoveredLicenseQuerySet(ProjectRelatedQuerySet):
+class DiscoveredLicenseQuerySet(
+ ComplianceAlertQuerySetMixin,
+ ProjectRelatedQuerySet,
+):
def order_by_count_and_expression(self):
"""Order by detection count and license expression (identifer) fields."""
return self.order_by("-detection_count", "identifier")
@@ -3550,7 +3582,6 @@ class DiscoveredLicense(
"""
A project's Discovered Licenses are the unique License Detection objects
discovered in the code under analysis.
-
"""
license_expression_field = "license_expression"
diff --git a/scanpipe/templates/scanpipe/includes/project_summary_level.html b/scanpipe/templates/scanpipe/includes/project_summary_level.html
index 2e46585a8..6ab505e55 100644
--- a/scanpipe/templates/scanpipe/includes/project_summary_level.html
+++ b/scanpipe/templates/scanpipe/includes/project_summary_level.html
@@ -14,6 +14,12 @@
{% endif %}
+ {% if project.package_compliance_alert_count %}
+
+ {{ project.package_compliance_alert_count|intcomma }}
+
+
+ {% endif %}
{% else %}
0
{% endif %}
@@ -40,20 +46,6 @@
-
Resources
diff --git a/scanpipe/templates/scanpipe/license_detection_list.html b/scanpipe/templates/scanpipe/license_detection_list.html
index 426c5b145..caedea9aa 100644
--- a/scanpipe/templates/scanpipe/license_detection_list.html
+++ b/scanpipe/templates/scanpipe/license_detection_list.html
@@ -24,23 +24,28 @@
{# CAUTION: Avoid relying on get_absolute_url to prevent unnecessary query triggers #}
{{ license_detection.identifier }}
+ {% if license_detection.has_compliance_alert %}
+
+
+
+ {% endif %}
- {{ license_detection.license_expression }}
+ {{ license_detection.license_expression }}
- {{ license_detection.license_expression_spdx }}
+ {{ license_detection.license_expression_spdx }}
{{ license_detection.detection_count }}
{% if display_compliance_alert %}
-
- {{ package.compliance_alert }}
+
+ {{ license_detection.compliance_alert }}
- {% endif %}
+ {% endif %}
{% empty %}
diff --git a/scanpipe/templates/scanpipe/package_list.html b/scanpipe/templates/scanpipe/package_list.html
index 8fc0654a1..9c277833d 100644
--- a/scanpipe/templates/scanpipe/package_list.html
+++ b/scanpipe/templates/scanpipe/package_list.html
@@ -30,6 +30,11 @@
{% endif %}
+ {% if package.has_compliance_alert %}
+
+
+
+ {% endif %}
diff --git a/scanpipe/templates/scanpipe/panels/license_detections_summary.html b/scanpipe/templates/scanpipe/panels/license_detections_summary.html
new file mode 100644
index 000000000..6e045491f
--- /dev/null
+++ b/scanpipe/templates/scanpipe/panels/license_detections_summary.html
@@ -0,0 +1,19 @@
+{% load humanize %}
+{% if license_detection_summary %}
+
+{% endif %}
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/panels/resource_license_summary.html b/scanpipe/templates/scanpipe/panels/resource_license_summary.html
deleted file mode 100644
index c2e158753..000000000
--- a/scanpipe/templates/scanpipe/panels/resource_license_summary.html
+++ /dev/null
@@ -1,16 +0,0 @@
-{% load humanize %}
-{% if resource_license_summary %}
-
-{% endif %}
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/project_detail.html b/scanpipe/templates/scanpipe/project_detail.html
index 3441fea8c..b398c2a83 100644
--- a/scanpipe/templates/scanpipe/project_detail.html
+++ b/scanpipe/templates/scanpipe/project_detail.html
@@ -104,7 +104,7 @@
{% if project.extra_data %}
diff --git a/scanpipe/templates/scanpipe/tabset/tab_packages.html b/scanpipe/templates/scanpipe/tabset/tab_packages.html
index 1370d741d..3231d4d86 100644
--- a/scanpipe/templates/scanpipe/tabset/tab_packages.html
+++ b/scanpipe/templates/scanpipe/tabset/tab_packages.html
@@ -18,6 +18,11 @@
{% endif %}
+ {% if package.has_compliance_alert %}
+
+
+
+ {% endif %}
{{ package.declared_license_expression }}
diff --git a/scanpipe/tests/test_views.py b/scanpipe/tests/test_views.py
index 7130bd3d5..bb3174317 100644
--- a/scanpipe/tests/test_views.py
+++ b/scanpipe/tests/test_views.py
@@ -683,7 +683,7 @@ def test_scanpipe_views_project_views(self):
with self.assertNumQueries(8):
self.client.get(url)
- with self.assertNumQueries(14):
+ with self.assertNumQueries(13):
self.client.get(self.project1.get_absolute_url())
@mock.patch("scanpipe.models.Run.execute_task_async")
@@ -915,7 +915,7 @@ def test_scanpipe_views_codebase_resource_views(self):
with self.assertNumQueries(7):
self.client.get(url)
- with self.assertNumQueries(7):
+ with self.assertNumQueries(8):
self.client.get(resource1.get_absolute_url())
def test_scanpipe_views_discovered_package_views(self):
diff --git a/scanpipe/urls.py b/scanpipe/urls.py
index 5469c7e1a..417064c4c 100644
--- a/scanpipe/urls.py
+++ b/scanpipe/urls.py
@@ -192,9 +192,9 @@
name="project_resource_status_summary",
),
path(
- "project//resource_license_summary/",
- views.ProjectResourceLicenseSummaryView.as_view(),
- name="project_resource_license_summary",
+ "project//license_detection_summary/",
+ views.ProjectLicenseDetectionSummaryView.as_view(),
+ name="project_license_detection_summary",
),
path(
"project//",
diff --git a/scanpipe/views.py b/scanpipe/views.py
index 11242acd7..6238e1890 100644
--- a/scanpipe/views.py
+++ b/scanpipe/views.py
@@ -944,14 +944,14 @@ def get_context_data(self, **kwargs):
return context
-class ProjectResourceLicenseSummaryView(ConditionalLoginRequired, generic.DetailView):
+class ProjectLicenseDetectionSummaryView(ConditionalLoginRequired, generic.DetailView):
model = Project
- template_name = "scanpipe/panels/resource_license_summary.html"
+ template_name = "scanpipe/panels/license_detections_summary.html"
@staticmethod
- def get_resource_license_summary(project, limit=10):
+ def get_license_detection_summary(project, limit=10):
license_counter = count_group_by(
- project.codebaseresources.files(), "detected_license_expression"
+ project.discoveredlicenses, "license_expression"
)
if list(license_counter.keys()) == [""]:
@@ -962,24 +962,29 @@ def get_resource_license_summary(project, limit=10):
sorted(license_counter.items(), key=operator.itemgetter(1), reverse=True)
)
- # Remove the "no licenses" entry from the top list
- no_licenses = sorted_by_count.pop("", None)
-
# Keep the top entries
top_licenses = dict(list(sorted_by_count.items())[:limit])
- # Add the "no licenses" entry at the end
- if no_licenses:
- top_licenses[""] = no_licenses
+ # Also get count for detections with
+ expressions_with_compliance_alert = []
+ for license_expression in top_licenses.keys():
+ has_compliance_alert = (
+ project.discoveredlicenses.filter(license_expression=license_expression)
+ .has_compliance_alert()
+ .exists()
+ )
+ if has_compliance_alert:
+ expressions_with_compliance_alert.append(license_expression)
- return top_licenses
+ return top_licenses, expressions_with_compliance_alert
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- summary = self.get_resource_license_summary(project=self.object)
- context["resource_license_summary"] = summary
- context["project_resources_url"] = reverse(
- "project_resources", args=[self.object.slug]
+ summary, expressions = self.get_license_detection_summary(project=self.object)
+ context["license_detection_summary"] = summary
+ context["expressions_with_compliance_alert"] = expressions
+ context["project_licenses_url"] = reverse(
+ "project_licenses", args=[self.object.slug]
)
return context
@@ -1544,7 +1549,10 @@ class DiscoveredLicenseListView(
"field_name": "license_expression",
"filter_fieldname": "license_expression",
},
- "license_expression_spdx",
+ {
+ "field_name": "license_expression_spdx",
+ "filter_fieldname": "license_expression_spdx",
+ },
"detection_count",
{
"field_name": "compliance_alert",
@@ -1566,6 +1574,11 @@ def get_queryset(self):
.order_by_count_and_expression()
)
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ context["display_compliance_alert"] = scanpipe_app.policies_enabled
+ return context
+
class ProjectMessageListView(
ConditionalLoginRequired,
From 973d7aa7698eaaeefa20731651b85e4d80443514 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Thu, 18 Jul 2024 03:00:03 +0530
Subject: [PATCH 10/12] Improve license detection and summary related views
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/filters.py | 6 +-
scanpipe/models.py | 33 +++-
.../includes/project_summary_level.html | 8 +-
.../scanpipe/license_detection_list.html | 2 +-
.../panels/license_detections_summary.html | 18 +-
.../scanpipe/panels/scan_summary_panel.html | 162 ++++++++++++++++--
.../tabset/tab_license_detections.html | 4 +-
scanpipe/tests/test_views.py | 19 +-
scanpipe/views.py | 49 ++++--
9 files changed, 245 insertions(+), 56 deletions(-)
diff --git a/scanpipe/filters.py b/scanpipe/filters.py
index b60888af5..307cfbfdc 100644
--- a/scanpipe/filters.py
+++ b/scanpipe/filters.py
@@ -492,6 +492,7 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
dropdown_widget_fields = [
"status",
"type",
+ "programming_language",
"tag",
"compliance_alert",
"in_package",
@@ -521,6 +522,7 @@ class ResourceFilterSet(FilterSetUtilsMixin, django_filters.FilterSet):
"related_from__from_resource__path",
],
)
+ programming_language = django_filters.AllValuesFilter()
compliance_alert = django_filters.ChoiceFilter(
choices=[(EMPTY_VAR, "None")] + CodebaseResource.Compliance.choices,
)
@@ -577,8 +579,8 @@ class Meta:
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
- license_expression_filer = self.filters["detected_license_expression"]
- license_expression_filer.extra["widget"] = HasValueDropdownWidget()
+ license_expression_filter = self.filters["detected_license_expression"]
+ license_expression_filter.extra["widget"] = HasValueDropdownWidget()
@classmethod
def filter_for_lookup(cls, field, lookup_type):
diff --git a/scanpipe/models.py b/scanpipe/models.py
index f47fed0ec..d2aed87ec 100644
--- a/scanpipe/models.py
+++ b/scanpipe/models.py
@@ -1365,14 +1365,28 @@ def license_detections_count(self):
@cached_property
def package_compliance_alert_count(self):
- """Return the number of packages related to this project which have."""
+ """
+ Return the number of packages related to this project which have
+ a license compliance error alert.
+ """
return self.discoveredpackages.has_compliance_alert().count()
@cached_property
def license_compliance_alert_count(self):
- """Return the number of packages related to this project which have."""
+ """
+ Return the number of license detections related to this project
+ which have a license compliance error alert.
+ """
return self.discoveredlicenses.has_compliance_alert().count()
+ @cached_property
+ def resource_compliance_alert_count(self):
+ """
+ Return the number of codebase resources related to this project which have
+ a license compliance error alert.
+ """
+ return self.codebaseresources.has_compliance_alert().count()
+
@cached_property
def message_count(self):
"""Return the number of messages related to this project."""
@@ -2042,7 +2056,15 @@ def convert_glob_to_django_regex(glob_pattern):
return escaped_pattern
-class CodebaseResourceQuerySet(ProjectRelatedQuerySet):
+class ComplianceAlertQuerySetMixin:
+ def has_compliance_alert(self):
+ return self.filter(Q(compliance_alert__exact=CodebaseResource.Compliance.ERROR))
+
+
+class CodebaseResourceQuerySet(
+ ComplianceAlertQuerySetMixin,
+ ProjectRelatedQuerySet,
+):
def prefetch_for_serializer(self):
"""
Optimized prefetching for a QuerySet to be consumed by the
@@ -2945,11 +2967,6 @@ def vulnerable(self):
return self.filter(~Q(affected_by_vulnerabilities__in=EMPTY_VALUES))
-class ComplianceAlertQuerySetMixin:
- def has_compliance_alert(self):
- return self.filter(Q(compliance_alert__exact=CodebaseResource.Compliance.ERROR))
-
-
class DiscoveredPackageQuerySet(
VulnerabilityQuerySetMixin,
ComplianceAlertQuerySetMixin,
diff --git a/scanpipe/templates/scanpipe/includes/project_summary_level.html b/scanpipe/templates/scanpipe/includes/project_summary_level.html
index 6ab505e55..e5c36665b 100644
--- a/scanpipe/templates/scanpipe/includes/project_summary_level.html
+++ b/scanpipe/templates/scanpipe/includes/project_summary_level.html
@@ -49,11 +49,17 @@
diff --git a/scanpipe/templates/scanpipe/panels/license_detections_summary.html b/scanpipe/templates/scanpipe/panels/license_detections_summary.html
index 6e045491f..6b71f5ed4 100644
--- a/scanpipe/templates/scanpipe/panels/license_detections_summary.html
+++ b/scanpipe/templates/scanpipe/panels/license_detections_summary.html
@@ -3,17 +3,29 @@
{% endif %}
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/panels/scan_summary_panel.html b/scanpipe/templates/scanpipe/panels/scan_summary_panel.html
index 52c11c1a2..1def8b4be 100644
--- a/scanpipe/templates/scanpipe/panels/scan_summary_panel.html
+++ b/scanpipe/templates/scanpipe/panels/scan_summary_panel.html
@@ -6,15 +6,80 @@
- {% for field_label, values in scan_summary.items %}
-
-
- {{ field_label }}
-
-
-
- {% for entry in values %}
- {% if entry.value %}
+
+
+ Declared license
+
+
+
+ {% for entry in scan_summary.declared_license_expression %}
+ {% if entry.value %}
+
+ {{ entry.value }}
+ {% if entry.count %}
+
+ {{ entry.count|intcomma }}
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ Declared holder
+
+
+
+ {% for entry in scan_summary.declared_holder %}
+ {% if entry.value %}
+
+ {{ entry.value }}
+ {% if entry.count %}
+
+ {{ entry.count|intcomma }}
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ Primary language
+
+
+
+
+
+
+
+ Other licenses
+
+
+
-
-
- {% endfor %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ Other holders
+
+
+
+ {% for entry in scan_summary.other_holders %}
+ {% if entry.value %}
+
+ {{ entry.value }}
+ {% if entry.count %}
+
+ {{ entry.count|intcomma }}
+
+ {% endif %}
+
+ {% endif %}
+ {% endfor %}
+
+
+
+
+
+ Other languages
+
+
+
+
+
+
+
+ Key Files
+
+
+
+
+
\ No newline at end of file
diff --git a/scanpipe/templates/scanpipe/tabset/tab_license_detections.html b/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
index c1fd375d7..e5b6fcd2e 100644
--- a/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
+++ b/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
@@ -19,7 +19,7 @@
{{ match.license_expression }}
- {{ match.from_file }}
+ {{ match.from_file }}
{{ match.rule_url }}
@@ -73,7 +73,7 @@
{% for file_region in tab_data.fields.file_regions.value %}
- {{ file_region.path }}
+ {{ file_region.path }}
{{ file_region.start_line }}
diff --git a/scanpipe/tests/test_views.py b/scanpipe/tests/test_views.py
index 5c762b325..128210501 100644
--- a/scanpipe/tests/test_views.py
+++ b/scanpipe/tests/test_views.py
@@ -475,16 +475,17 @@ def test_scanpipe_views_project_details_get_scan_summary_data(self):
scan_summary = self.data / "scancode" / "is-npm-1.0.0_scan_package_summary.json"
scan_summary_json = json.loads(scan_summary.read_text())
- scan_summary_data = get_scan_summary_data(scan_summary_json)
+ scan_summary_data = get_scan_summary_data(self.project1, scan_summary_json)
- self.assertEqual(6, len(scan_summary_data))
+ self.assertEqual(7, len(scan_summary_data))
expected = [
- "Declared license",
- "Declared holder",
- "Primary language",
- "Other licenses",
- "Other holders",
- "Other languages",
+ "declared_license_expression",
+ "declared_holder",
+ "primary_language",
+ "other_license_expressions",
+ "other_holders",
+ "other_languages",
+ "key_file_licenses",
]
self.assertEqual(expected, list(scan_summary_data.keys()))
@@ -956,7 +957,7 @@ def test_scanpipe_views_codebase_resource_views(self):
package1.add_resources([resource1, resource2])
url = reverse("project_resources", args=[self.project1.slug])
- with self.assertNumQueries(8):
+ with self.assertNumQueries(9):
self.client.get(url)
with self.assertNumQueries(8):
diff --git a/scanpipe/views.py b/scanpipe/views.py
index 00c597690..8df4d1bc0 100644
--- a/scanpipe/views.py
+++ b/scanpipe/views.py
@@ -173,12 +173,12 @@
SCAN_SUMMARY_FIELDS = [
- ("Declared license", "declared_license_expression"),
- ("Declared holder", "declared_holder"),
- ("Primary language", "primary_language"),
- ("Other licenses", "other_license_expressions"),
- ("Other holders", "other_holders"),
- ("Other languages", "other_languages"),
+ "declared_license_expression",
+ "declared_holder",
+ "primary_language",
+ "other_license_expressions",
+ "other_holders",
+ "other_languages",
]
@@ -317,7 +317,9 @@ def get_field_value(self, field_name, render_func=None):
"""
Return the formatted value of the specified `field_name` from the object.
- By default, JSON types (list and dict) are rendered as YAML.
+ By default, JSON types (list and dict) are rendered as YAML,
+ except some fields which are used for a more complex tabular
+ representation with links to other views.
If a `render_func` is provided, it will take precedence and be used for
rendering the value.
"""
@@ -652,11 +654,12 @@ def get_license_clarity_data(scan_summary_json):
]
@staticmethod
- def get_scan_summary_data(scan_summary_json):
+ def get_scan_summary_data(project, scan_summary_json):
summary_data = {}
- for field_label, field_name in SCAN_SUMMARY_FIELDS:
- field_data = scan_summary_json.get(field_name)
+ for field_name, field_data in scan_summary_json.items():
+ if field_name not in SCAN_SUMMARY_FIELDS:
+ continue
if type(field_data) is list:
# Do not include `None` entries
@@ -665,7 +668,13 @@ def get_scan_summary_data(scan_summary_json):
# Converts single value type into common data-structure
values = [{"value": field_data}]
- summary_data[field_label] = values
+ summary_data[field_name] = values
+
+ key_files = project.codebaseresources.filter(is_key_file=True)
+ summary_data["key_file_licenses"] = {
+ key_file.path: key_file.detected_license_expression
+ for key_file in key_files
+ }
return summary_data
@@ -723,7 +732,7 @@ def get_context_data(self, **kwargs):
with suppress(json.decoder.JSONDecodeError):
scan_summary_json = json.loads(scan_summary_file.read_text())
license_clarity = self.get_license_clarity_data(scan_summary_json)
- scan_summary = self.get_scan_summary_data(scan_summary_json)
+ scan_summary = self.get_scan_summary_data(project, scan_summary_json)
codebase_root = sorted(
project.codebase_path.glob("*"),
@@ -982,7 +991,7 @@ def get_license_detection_summary(project, limit=10):
)
if list(license_counter.keys()) == [""]:
- return
+ return None, None, None
# Order the license list by the number of detections, higher first
sorted_by_count = dict(
@@ -1003,13 +1012,23 @@ def get_license_detection_summary(project, limit=10):
if has_compliance_alert:
expressions_with_compliance_alert.append(license_expression)
- return top_licenses, expressions_with_compliance_alert
+ total_counts = {
+ "with_compliance_error": (
+ project.discoveredlicenses.has_compliance_alert().count()
+ ),
+ "all": project.discoveredlicenses.count(),
+ }
+
+ return top_licenses, expressions_with_compliance_alert, total_counts
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
- summary, expressions = self.get_license_detection_summary(project=self.object)
+ summary, expressions, counts = self.get_license_detection_summary(
+ project=self.object
+ )
context["license_detection_summary"] = summary
context["expressions_with_compliance_alert"] = expressions
+ context["total_counts"] = counts
context["project_licenses_url"] = reverse(
"project_licenses", args=[self.object.slug]
)
From 9f220accaa275c8a3fcb52ad82533e1ec5e4b133 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Sat, 7 Sep 2024 00:43:33 +0530
Subject: [PATCH 11/12] Add license detection improvements
Signed-off-by: Ayan Sinha Mahapatra
---
.../scanpipe/license_detection_list.html | 2 +-
.../panels/license_detections_summary.html | 4 +--
.../tabset/tab_license_detections.html | 31 +++++++++++++------
.../tabset/tab_package_detections.html | 14 ++++-----
.../tabset/tab_resource_detections.html | 18 +++++------
5 files changed, 40 insertions(+), 29 deletions(-)
diff --git a/scanpipe/templates/scanpipe/license_detection_list.html b/scanpipe/templates/scanpipe/license_detection_list.html
index ea04ab67b..6b5ced789 100644
--- a/scanpipe/templates/scanpipe/license_detection_list.html
+++ b/scanpipe/templates/scanpipe/license_detection_list.html
@@ -50,7 +50,7 @@
{% empty %}
- No Licenses detected. Clear search and filters
+ No licenses detected. Clear search and filters
{% endfor %}
diff --git a/scanpipe/templates/scanpipe/panels/license_detections_summary.html b/scanpipe/templates/scanpipe/panels/license_detections_summary.html
index 6b71f5ed4..91bba8086 100644
--- a/scanpipe/templates/scanpipe/panels/license_detections_summary.html
+++ b/scanpipe/templates/scanpipe/panels/license_detections_summary.html
@@ -3,7 +3,7 @@
- Unique License detections
+ Unique license detections
{% for license_expression, count in license_detection_summary.items %}
@@ -16,7 +16,7 @@
{% endfor %}
{% if total_counts.all %}
- See all License Detections
+ See all license detections
{{ total_counts.all|intcomma }}
{% if total_counts.with_compliance_error %}
diff --git a/scanpipe/templates/scanpipe/tabset/tab_license_detections.html b/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
index e5b6fcd2e..6903978b9 100644
--- a/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
+++ b/scanpipe/templates/scanpipe/tabset/tab_license_detections.html
@@ -2,14 +2,15 @@
- License Expression
- Origin Resource Path
+ License expression
+ Origin resource path
+ Matched text
Rule URL
Score
Matcher
- Match Length
- Match Coverage
- Rule Relevance
+ Match length
+ Match coverage
+ Rule relevance
@@ -22,7 +23,17 @@
{{ match.from_file }}
- {{ match.rule_url }}
+ {{ match.matched_text }}
+
+
+ {% if match.rule_url %}
+
+ {{ match.rule_identifier }}
+
+
+ {% else %}
+ {{ match.rule_identifier }}
+ {% endif %}
{{ match.score }}
@@ -47,7 +58,7 @@
- Detection Log
+ Detection log
@@ -64,9 +75,9 @@
- Resource Path
- Start Line
- End Line
+ Resource path
+ Start line
+ End line
diff --git a/scanpipe/templates/scanpipe/tabset/tab_package_detections.html b/scanpipe/templates/scanpipe/tabset/tab_package_detections.html
index af09167b8..c033a74a6 100644
--- a/scanpipe/templates/scanpipe/tabset/tab_package_detections.html
+++ b/scanpipe/templates/scanpipe/tabset/tab_package_detections.html
@@ -2,7 +2,7 @@
- Datafile Paths
+ Datafile paths
@@ -35,9 +35,9 @@
- License Detections
- License Expression
- License Expression SPDX
+ License detections
+ License expression
+ License expression SPDX
@@ -61,9 +61,9 @@
- License Detections
- License Expression
- License Expression SPDX
+ License detections
+ License expression
+ License expression SPDX
diff --git a/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html b/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
index 4cd3c37ed..d5165a3f2 100644
--- a/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
+++ b/scanpipe/templates/scanpipe/tabset/tab_resource_detections.html
@@ -3,9 +3,9 @@
- License Detections
- License Expression
- License Expression SPDX
+ License detections
+ License expression
+ License expression SPDX
@@ -29,8 +29,8 @@
- License Expression
- License Clue Detials
+ License expression
+ License clue detials
@@ -52,8 +52,8 @@
Email
- Start Line
- End Line
+ Start line
+ End line
@@ -78,8 +78,8 @@
URL
- Start Line
- End Line
+ Start line
+ End line
From b1fad21471334d846df661eb3c25b5c1d234f0a2 Mon Sep 17 00:00:00 2001
From: Ayan Sinha Mahapatra
Date: Thu, 3 Jul 2025 17:19:51 +0530
Subject: [PATCH 12/12] Add license detections to Compliance alerts panel
Signed-off-by: Ayan Sinha Mahapatra
---
scanpipe/pipes/compliance.py | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/scanpipe/pipes/compliance.py b/scanpipe/pipes/compliance.py
index 0a0c0c776..79be3f3f4 100644
--- a/scanpipe/pipes/compliance.py
+++ b/scanpipe/pipes/compliance.py
@@ -105,6 +105,11 @@ def get_project_compliance_alerts(project, fail_level="error"):
.only(*PACKAGE_URL_FIELDS, "compliance_alert")
.order_by(*PACKAGE_URL_FIELDS)
)
+ licenses_qs = (
+ project.discoveredlicenses.compliance_issues(severity=fail_level)
+ .only("identifier", "compliance_alert")
+ .order_by("identifier")
+ )
resource_qs = (
project.codebaseresources.compliance_issues(severity=fail_level)
.only("path", "compliance_alert")
@@ -113,6 +118,7 @@ def get_project_compliance_alerts(project, fail_level="error"):
queryset_mapping = {
"packages": package_qs,
+ "license_detections": licenses_qs,
"resources": resource_qs,
}