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 %}
+
+
-
- {{ vulnerability.vulnerability_id }}
-
-
-
- {% for alias in aliases %}
- -
- {% if alias|slice:":3" == "CVE" %}
- {{ alias }}
-
-
- {% elif alias|slice:":4" == "GHSA" %}
- {{ alias }}
-
-
- {% elif alias|slice:":3" == "NPM" %}
- {{ alias }}
-
-
- {% else %}
- {{ alias }}
- {% endif %}
-
- {% endfor %}
-
+ {% 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/list_view_thead.html' %}
+
+ {% for vulnerability in object_list.values %}
+
+ |
+ {% 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 %}
+ |
+
+ {% empty %}
+
+ |
+ No Vulnerabilities found. Clear search and filters
+ |
+
+ {% endfor %}
+
+
+
+{% 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,
|