Skip to content

Commit 30c1d8e

Browse files
authored
Add a PurlDB tab in the Package details view #1125 (#1128)
Signed-off-by: tdruez <tdruez@nexb.com>
1 parent 9420ec9 commit 30c1d8e

File tree

8 files changed

+154
-23
lines changed

8 files changed

+154
-23
lines changed

CHANGELOG.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ v34.1.0 (unreleased)
3939
A data migration is included to facilitate the migration of existing data.
4040
https://github.com/nexB/scancode.io/issues/1099
4141

42+
- Add PurlDB tab, displayed when the PURLDB_URL settings is configured.
43+
When loading the package details view, a request is made on the PurlDB to fetch and
44+
and display any available data.
45+
https://github.com/nexB/scancode.io/issues/1125
46+
4247
v34.0.0 (2024-03-04)
4348
--------------------
4449

docs/application-settings.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,10 @@ API key using ``PURLDB_API_KEY``::
294294

295295
PURLDB_API_KEY=insert_your_api_key_here
296296

297+
.. note::
298+
Once the PurlDB is configured, a new "PurlDB" tab will be available in the
299+
discovered package details view.
300+
297301
.. _scancodeio_settings_vulnerablecode:
298302

299303
VULNERABLECODE

scanpipe/pipes/purldb.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,3 +330,17 @@ def populate_purldb_with_discovered_dependencies(project, logger=logger.info):
330330
chunk_size=10,
331331
logger=logger,
332332
)
333+
334+
335+
def get_package_by_purl(package_url):
336+
"""Get a Package details entry providing its `package_url`."""
337+
if results := find_packages({"purl": str(package_url)}):
338+
return results[0]
339+
340+
341+
def find_packages(payload):
342+
"""Get Packages using provided `payload` filters on the PurlDB package list."""
343+
package_api_url = f"{PURLDB_API_URL}packages/"
344+
response = request_get(package_api_url, payload=payload)
345+
if response and response.get("count") > 0:
346+
return response.get("results")
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{% if tab_data.fields %}
2+
<div class="notification is-link is-light has-text-weight-semibold p-3 mb-4">
3+
<i class="fa-solid fa-circle-info mr-1"></i>
4+
You are looking at the details for this software package as defined
5+
in the PurlDB which was scanned automatically from a public source.
6+
</div>
7+
{% include 'scanpipe/tabset/tab_default.html' %}
8+
{% else %}
9+
<div class="notification is-warning is-light has-text-weight-semibold p-3 mb-4">
10+
<i class="fa-solid fa-triangle-exclamation mr-1"></i>
11+
No entries found in the PurlDB for this package.
12+
</div>
13+
{% endif %}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<div hx-get="{% url 'package_purldb_tab' project.slug object.uuid %}" hx-trigger="load" hx-swap="outerHTML">
2+
<div class="notification has-text-weight-semibold p-3">
3+
<i class="fas fa-spinner fa-spin"></i> Fetching {{ object }} in {{ tab_data.verbose_name }} ...
4+
</div>
5+
</div>

scanpipe/tests/test_views.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,41 @@ def test_scanpipe_views_discovered_package_details_view_tab_vulnerabilities(self
979979
self.assertContains(response, '<section id="tab-vulnerabilities"')
980980
self.assertContains(response, "VCID-cah8-awtr-aaad")
981981

982+
@mock.patch("scanpipe.pipes.purldb.is_configured")
983+
def test_scanpipe_views_discovered_package_purldb_tab_view(self, mock_configured):
984+
package1 = DiscoveredPackage.create_from_data(self.project1, package_data1)
985+
package_url = package1.get_absolute_url()
986+
987+
mock_configured.return_value = False
988+
response = self.client.get(package_url)
989+
self.assertNotContains(response, "tab-purldb")
990+
self.assertNotContains(response, '<section id="tab-purldb"')
991+
992+
mock_configured.return_value = True
993+
response = self.client.get(package_url)
994+
self.assertContains(response, "tab-purldb")
995+
self.assertContains(response, '<section id="tab-purldb"')
996+
997+
with mock.patch("scanpipe.pipes.purldb.get_package_by_purl") as get_package:
998+
get_package.return_value = None
999+
purldb_tab_url = f"{package_url}purldb_tab/"
1000+
response = self.client.get(purldb_tab_url)
1001+
msg = "No entries found in the PurlDB for this package"
1002+
self.assertContains(response, msg)
1003+
1004+
get_package.return_value = {
1005+
"uuid": "9261605f-e2fb-4db9-94ab-0d82d3273cdf",
1006+
"filename": "abab-2.0.3.tgz",
1007+
"type": "npm",
1008+
"name": "abab",
1009+
"version": "2.0.3",
1010+
"primary_language": "JavaScript",
1011+
}
1012+
response = self.client.get(purldb_tab_url)
1013+
self.assertContains(response, "abab-2.0.3.tgz")
1014+
self.assertContains(response, "2.0.3")
1015+
self.assertContains(response, "JavaScript")
1016+
9821017
def test_scanpipe_views_discovered_dependency_views(self):
9831018
DiscoveredPackage.create_from_data(self.project1, package_data1)
9841019
make_resource_file(

scanpipe/urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@
4646
views.CodebaseResourceListView.as_view(),
4747
name="project_resources",
4848
),
49+
path(
50+
"project/<slug:slug>/packages/<uuid:uuid>/purldb_tab/",
51+
views.DiscoveredPackagePurlDBTabView.as_view(),
52+
name="package_purldb_tab",
53+
),
4954
path(
5055
"project/<slug:slug>/packages/<uuid:uuid>/",
5156
views.DiscoveredPackageDetailsView.as_view(),

scanpipe/views.py

Lines changed: 73 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from django.urls import reverse
5050
from django.urls import reverse_lazy
5151
from django.utils.decorators import method_decorator
52+
from django.utils.text import capfirst
5253
from django.views import generic
5354
from django.views.decorators.http import require_POST
5455
from django.views.generic.detail import SingleObjectMixin
@@ -89,6 +90,7 @@
8990
from scanpipe.models import RunInProgressError
9091
from scanpipe.pipes import count_group_by
9192
from scanpipe.pipes import output
93+
from scanpipe.pipes import purldb
9294

9395
scanpipe_app = apps.get_app_config("scanpipe")
9496

@@ -177,6 +179,10 @@
177179
]
178180

179181

182+
def purldb_is_configured(*args):
183+
return purldb.is_configured()
184+
185+
180186
class PrefetchRelatedViewMixin:
181187
prefetch_related = []
182188

@@ -1631,22 +1637,22 @@ class CodebaseResourceDetailsView(
16311637
"tag",
16321638
"rootfs_path",
16331639
],
1634-
"icon_class": "fa-solid fa-info-circle",
1640+
"icon_class": "fa-solid fa-circle-check",
16351641
},
16361642
"others": {
16371643
"fields": [
16381644
{"field_name": "size", "render_func": filesizeformat},
1639-
"md5",
1640-
"sha1",
1641-
"sha256",
1642-
"sha512",
1645+
{"field_name": "md5", "label": "MD5"},
1646+
{"field_name": "sha1", "label": "SHA1"},
1647+
{"field_name": "sha256", "label": "SHA256"},
1648+
{"field_name": "sha512", "label": "SHA512"},
16431649
"is_binary",
16441650
"is_text",
16451651
"is_archive",
16461652
"is_key_file",
16471653
"is_media",
16481654
],
1649-
"icon_class": "fa-solid fa-plus-square",
1655+
"icon_class": "fa-solid fa-info-circle",
16501656
},
16511657
"viewer": {
16521658
"icon_class": "fa-solid fa-file-code",
@@ -1692,7 +1698,7 @@ class CodebaseResourceDetailsView(
16921698
{"field_name": "extra_data", "render_func": render_as_yaml},
16931699
],
16941700
"verbose_name": "Extra",
1695-
"icon_class": "fa-solid fa-database",
1701+
"icon_class": "fa-solid fa-plus-square",
16961702
},
16971703
}
16981704

@@ -1828,23 +1834,25 @@ class DiscoveredPackageDetailsView(
18281834
"description",
18291835
"tag",
18301836
],
1831-
"icon_class": "fa-solid fa-info-circle",
1837+
"icon_class": "fa-solid fa-circle-check",
18321838
},
18331839
"others": {
18341840
"fields": [
18351841
{"field_name": "size", "render_func": filesizeformat},
18361842
"release_date",
1837-
"md5",
1838-
"sha1",
1839-
"sha256",
1840-
"sha512",
1843+
{"field_name": "md5", "label": "MD5"},
1844+
{"field_name": "sha1", "label": "SHA1"},
1845+
{"field_name": "sha256", "label": "SHA256"},
1846+
{"field_name": "sha512", "label": "SHA512"},
18411847
"file_references",
18421848
{"field_name": "parties", "render_func": render_as_yaml},
18431849
"missing_resources",
18441850
"modified_resources",
18451851
"package_uid",
1852+
"datasource_ids",
1853+
"datafile_paths",
18461854
],
1847-
"icon_class": "fa-solid fa-plus-square",
1855+
"icon_class": "fa-solid fa-info-circle",
18481856
},
18491857
"terms": {
18501858
"fields": [
@@ -1870,14 +1878,6 @@ class DiscoveredPackageDetailsView(
18701878
],
18711879
"icon_class": "fa-solid fa-file-contract",
18721880
},
1873-
"detection": {
1874-
"fields": [
1875-
"datasource_ids",
1876-
"datafile_paths",
1877-
],
1878-
"icon_class": "fa-solid fa-search",
1879-
"template": "scanpipe/tabset/tab_detections.html",
1880-
},
18811881
"resources": {
18821882
"fields": ["codebase_resources"],
18831883
"icon_class": "fa-solid fa-folder-open",
@@ -1898,11 +1898,61 @@ class DiscoveredPackageDetailsView(
18981898
{"field_name": "extra_data", "render_func": render_as_yaml},
18991899
],
19001900
"verbose_name": "Extra",
1901+
"icon_class": "fa-solid fa-plus-square",
1902+
},
1903+
"purldb": {
1904+
"fields": ["uuid"],
1905+
"verbose_name": "PurlDB",
19011906
"icon_class": "fa-solid fa-database",
1907+
"template": "scanpipe/tabset/tab_purldb_loader.html",
1908+
"display_condition": purldb_is_configured,
19021909
},
19031910
}
19041911

19051912

1913+
class DiscoveredPackagePurlDBTabView(ConditionalLoginRequired, generic.DetailView):
1914+
model = DiscoveredPackage
1915+
slug_field = "uuid"
1916+
slug_url_kwarg = "uuid"
1917+
template_name = "scanpipe/tabset/tab_purldb_content.html"
1918+
1919+
@staticmethod
1920+
def get_fields_data(purldb_entry):
1921+
exclude = [
1922+
"uuid",
1923+
"purl",
1924+
"license_detections",
1925+
"resources",
1926+
]
1927+
1928+
fields_data = {}
1929+
for field_name, value in purldb_entry.items():
1930+
if not value or field_name in exclude:
1931+
continue
1932+
1933+
label = capfirst(
1934+
field_name.replace("url", "URL")
1935+
.replace("_", " ")
1936+
.replace("sha", "SHA")
1937+
.replace("vcs", "VCS")
1938+
)
1939+
fields_data[field_name] = {"label": label, "value": value}
1940+
1941+
return fields_data
1942+
1943+
def get_context_data(self, **kwargs):
1944+
context = super().get_context_data(**kwargs)
1945+
1946+
if not purldb.is_configured():
1947+
raise Http404("PurlDB access is not configured.")
1948+
1949+
if purldb_entry := purldb.get_package_by_purl(self.object.package_url):
1950+
fields = self.get_fields_data(purldb_entry)
1951+
context["tab_data"] = {"fields": fields}
1952+
1953+
return context
1954+
1955+
19061956
class DiscoveredDependencyDetailsView(
19071957
ConditionalLoginRequired,
19081958
ProjectRelatedViewMixin,
@@ -1944,7 +1994,7 @@ class DiscoveredDependencyDetailsView(
19441994
"scope",
19451995
"datasource_id",
19461996
],
1947-
"icon_class": "fa-solid fa-info-circle",
1997+
"icon_class": "fa-solid fa-circle-check",
19481998
},
19491999
"others": {
19502000
"fields": [
@@ -1954,7 +2004,7 @@ class DiscoveredDependencyDetailsView(
19542004
"is_optional",
19552005
"is_resolved",
19562006
],
1957-
"icon_class": "fa-solid fa-plus-square",
2007+
"icon_class": "fa-solid fa-info-circle",
19582008
},
19592009
"vulnerabilities": {
19602010
"fields": ["affected_by_vulnerabilities"],

0 commit comments

Comments
 (0)