diff --git a/scancodeio/static/main.css b/scancodeio/static/main.css index e8f45f1bf8..a51269fcbe 100644 --- a/scancodeio/static/main.css +++ b/scancodeio/static/main.css @@ -391,6 +391,12 @@ progress.file-upload::before { #message-list th#column-severity { min-width: 110px; } +th#column-vulnerability_id { + min-width: 220px; +} +th#column-summary { + width: 40%; +} .menu.is-info .is-active { background-color: #3e8ed0; } diff --git a/scanpipe/management/commands/check-compliance.py b/scanpipe/management/commands/check-compliance.py index 216a849e67..0016177a01 100644 --- a/scanpipe/management/commands/check-compliance.py +++ b/scanpipe/management/commands/check-compliance.py @@ -104,23 +104,17 @@ def check_compliance(self, fail_level): return total_issues > 0 def check_vulnerabilities(self): - packages = self.project.discoveredpackages.vulnerable_ordered() - dependencies = self.project.discovereddependencies.vulnerable_ordered() - - vulnerable_records = list(packages) + list(dependencies) - count = len(vulnerable_records) + all_vulnerabilities = self.project.vulnerabilities + vulnerabilities_count = len(all_vulnerabilities) if self.verbosity > 0: - if count: - self.stderr.write(f"{count} vulnerable records found:") - for entry in vulnerable_records: - self.stderr.write(str(entry)) - vulnerability_ids = [ - vulnerability.get("vulnerability_id") - for vulnerability in entry.affected_by_vulnerabilities - ] - self.stderr.write(" > " + ", ".join(vulnerability_ids)) + if vulnerabilities_count: + self.stderr.write(f"{vulnerabilities_count} vulnerabilities found:") + for vulnerability_id, vulnerability_data in all_vulnerabilities.items(): + self.stderr.write(str(vulnerability_id)) + for affected_obj in vulnerability_data.get("affects", []): + self.stderr.write(f" > {affected_obj}") else: self.stdout.write("No vulnerabilities found") - return count > 0 + return vulnerabilities_count > 0 diff --git a/scanpipe/models.py b/scanpipe/models.py index 6f3c5f550c..ac8e2da155 100644 --- a/scanpipe/models.py +++ b/scanpipe/models.py @@ -1495,6 +1495,11 @@ def vulnerable_dependency_count(self): """Return the number of vulnerable dependencies related to this project.""" return self.vulnerable_dependencies.count() + @cached_property + def vulnerability_count(self): + """Return the number of vulnerabilities related to this project.""" + return self.vulnerable_package_count + self.vulnerable_dependency_count + @cached_property def dependency_count(self): """Return the number of dependencies related to this project.""" diff --git a/scanpipe/templates/scanpipe/includes/project_summary_level.html b/scanpipe/templates/scanpipe/includes/project_summary_level.html index f942ebb418..55b40ae31c 100644 --- a/scanpipe/templates/scanpipe/includes/project_summary_level.html +++ b/scanpipe/templates/scanpipe/includes/project_summary_level.html @@ -94,4 +94,8 @@ {% endif %} {% url 'project_messages' project.slug as project_messages_url %} {% include "scanpipe/includes/project_summary_level_item.html" with label="Messages" count=project.message_count url=project_messages_url only %} + {% if project.vulnerability_count %} + {% url 'project_vulnerabilities' project.slug as project_vulnerabilities_url %} + {% include "scanpipe/includes/project_summary_level_item.html" with label="Vulnerabilities" count=project.vulnerability_count url=project_vulnerabilities_url only %} + {% endif %} \ No newline at end of file diff --git a/scanpipe/templates/scanpipe/includes/vulnerability_id.html b/scanpipe/templates/scanpipe/includes/vulnerability_id.html new file mode 100644 index 0000000000..51b3b18163 --- /dev/null +++ b/scanpipe/templates/scanpipe/includes/vulnerability_id.html @@ -0,0 +1,30 @@ +{% if vulnerability.vulnerability_id|slice:":4" == "VCID" and VULNERABLECODE_URL %} + + {{ vulnerability.vulnerability_id }} + + +{% else %} + {{ vulnerability.vulnerability_id }} +{% endif %} + + \ No newline at end of file diff --git a/scanpipe/templates/scanpipe/includes/vulnerability_summary.html b/scanpipe/templates/scanpipe/includes/vulnerability_summary.html new file mode 100644 index 0000000000..79e249d3fd --- /dev/null +++ b/scanpipe/templates/scanpipe/includes/vulnerability_summary.html @@ -0,0 +1,10 @@ +{% if vulnerability.summary %} + {% if vulnerability.summary|length > 150 %} +
+ {{ vulnerability.summary|slice:":150" }}... + {{ vulnerability.summary|slice:"150:" }} +
+ {% else %} + {{ vulnerability.summary }} + {% endif %} +{% endif %} \ No newline at end of file diff --git a/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html b/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html index a2ffb2b7c9..0d7a5e4053 100644 --- a/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html +++ b/scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html @@ -11,43 +11,10 @@ {% for vulnerability in tab_data.fields.affected_by_vulnerabilities.value %} - - {{ vulnerability.vulnerability_id }} - - - + {% include 'scanpipe/includes/vulnerability_id.html' with vulnerability=vulnerability %} - {% if vulnerability.summary %} - {% if vulnerability.summary|length > 150 %} -
- {{ vulnerability.summary|slice:":150" }}... - {{ vulnerability.summary|slice:"150:" }} -
- {% else %} - {{ vulnerability.summary }} - {% endif %} - {% endif %} + {% include 'scanpipe/includes/vulnerability_summary.html' with vulnerability=vulnerability only %} {% for key, value in vulnerability.cdx_vulnerability.analysis.items %} diff --git a/scanpipe/templates/scanpipe/vulnerability_list.html b/scanpipe/templates/scanpipe/vulnerability_list.html new file mode 100644 index 0000000000..4ec9b7fce8 --- /dev/null +++ b/scanpipe/templates/scanpipe/vulnerability_list.html @@ -0,0 +1,43 @@ +{% extends "scanpipe/base.html" %} +{% load humanize %} + +{% block title %}ScanCode.io: Vulnerabilities{% endblock %} + +{% block content %} +
+ {% include 'scanpipe/includes/navbar_header.html' %} +
+ {% include 'scanpipe/includes/breadcrumb.html' with linked_project=True current="Vulnerabilities" %} +
{{ object_list|length|intcomma }} results
+
+
+ +
+ + {% include 'scanpipe/includes/list_view_thead.html' %} + + {% for vulnerability in object_list.values %} + + + + + + {% empty %} + + + + {% endfor %} + +
+ {% include 'scanpipe/includes/vulnerability_id.html' with vulnerability=vulnerability %} + + {% include 'scanpipe/includes/vulnerability_summary.html' with vulnerability=vulnerability only %} + + {% for obj in vulnerability.affects %} + {{ obj }}
+ {% endfor %} +
+ No Vulnerabilities found. Clear search and filters +
+
+{% endblock %} \ No newline at end of file diff --git a/scanpipe/tests/test_commands.py b/scanpipe/tests/test_commands.py index 15dc32449b..7e93bc1c64 100644 --- a/scanpipe/tests/test_commands.py +++ b/scanpipe/tests/test_commands.py @@ -1386,11 +1386,10 @@ def test_scanpipe_management_command_check_compliance_vulnerabilities(self): self.assertEqual(cm.exception.code, 1) out_value = out.getvalue().strip() expected = ( - "2 vulnerable records found:\n" - "pkg:generic/name@1.0\n" - " > VCID-cah8-awtr-aaad\n" - "dependency1\n" - " > VCID-cah8-awtr-aaad" + "1 vulnerabilities found:\n" + "VCID-cah8-awtr-aaad\n" + " > pkg:generic/name@1.0\n" + " > dependency1" ) self.assertEqual(expected, out_value) diff --git a/scanpipe/tests/test_models.py b/scanpipe/tests/test_models.py index 6f5a3acf0b..e4a5b4cb7d 100644 --- a/scanpipe/tests/test_models.py +++ b/scanpipe/tests/test_models.py @@ -678,6 +678,7 @@ def test_scanpipe_project_vulnerability_properties(self): "VCID-3": {"vulnerability_id": "VCID-3", "affects": [p2, d2]}, } self.assertEqual(expected, project.vulnerabilities) + self.assertEqual(4, project.vulnerability_count) def test_scanpipe_project_get_codebase_config_directory(self): self.assertIsNone(self.project1.get_codebase_config_directory()) diff --git a/scanpipe/tests/test_views.py b/scanpipe/tests/test_views.py index 9172c42410..63e9b9f295 100644 --- a/scanpipe/tests/test_views.py +++ b/scanpipe/tests/test_views.py @@ -821,7 +821,7 @@ def test_scanpipe_views_project_views(self): with self.assertNumQueries(7): self.client.get(url) - with self.assertNumQueries(13): + with self.assertNumQueries(15): self.client.get(self.project1.get_absolute_url()) @mock.patch("scanpipe.models.Run.execute_task_async") @@ -1276,6 +1276,31 @@ def test_scanpipe_views_project_message_views(self): with self.assertNumQueries(5): self.client.get(url) + @override_settings(VULNERABLECODE_URL="https://vcio/") + def test_scanpipe_views_vulnerability_list_view(self): + self.assertEqual(0, self.project1.vulnerability_count) + url = reverse("project_vulnerabilities", args=[self.project1.slug]) + with self.assertNumQueries(5): + response = self.client.get(url) + self.assertContains(response, "No Vulnerabilities found.") + + v1 = {"vulnerability_id": "VCID-1"} + v2 = {"vulnerability_id": "VCID-2"} + project = make_project() + make_package(project, "pkg:type/a", affected_by_vulnerabilities=[v1]) + make_dependency(project, affected_by_vulnerabilities=[v2]) + + self.assertEqual(2, project.vulnerability_count) + url = reverse("project_vulnerabilities", args=[project.slug]) + with self.assertNumQueries(5): + response = self.client.get(url) + + expected = '' + self.assertContains(response, expected) + expected = '' + self.assertContains(response, expected) + self.assertContains(response, "pkg:type/a") + def test_scanpipe_views_license_list_view(self): url = reverse("license_list") response = self.client.get(url) diff --git a/scanpipe/urls.py b/scanpipe/urls.py index 05a8a8a8ef..c0d47fbb2c 100644 --- a/scanpipe/urls.py +++ b/scanpipe/urls.py @@ -96,6 +96,11 @@ views.ProjectMessageListView.as_view(), name="project_messages", ), + path( + "project//vulnerabilities/", + views.VulnerabilityListView.as_view(), + name="project_vulnerabilities", + ), path( "project//archive/", views.ProjectArchiveView.as_view(), diff --git a/scanpipe/views.py b/scanpipe/views.py index 9913d4947f..5e657b874b 100644 --- a/scanpipe/views.py +++ b/scanpipe/views.py @@ -1626,7 +1626,7 @@ def get_queryset(self): def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) - context["project"] = self.project + context["project"] = self.get_project() context["model_label"] = self.model_label return context @@ -1951,6 +1951,28 @@ def get_filterset_kwargs(self, filterset_class): return kwargs +class VulnerabilityListView( + ConditionalLoginRequired, + ProjectRelatedViewMixin, + TableColumnsMixin, + generic.ListView, +): + template_name = "scanpipe/vulnerability_list.html" + table_columns = [ + "vulnerability_id", + "summary", + "affects", + ] + + def get_queryset(self): + return [] + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["object_list"] = self.project.vulnerabilities + return context + + class CodebaseResourceDetailsView( ConditionalLoginRequired, ProjectRelatedViewMixin,