Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions scancodeio/static/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
24 changes: 9 additions & 15 deletions scanpipe/management/commands/check-compliance.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 5 additions & 0 deletions scanpipe/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
</nav>
30 changes: 30 additions & 0 deletions scanpipe/templates/scanpipe/includes/vulnerability_id.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{% if vulnerability.vulnerability_id|slice:":4" == "VCID" and VULNERABLECODE_URL %}
<a href="{{ VULNERABLECODE_URL }}/vulnerabilities/{{ vulnerability.vulnerability_id }}" target="_blank">
{{ vulnerability.vulnerability_id }}
<i class="fa-solid fa-up-right-from-square is-small"></i>
</a>
{% else %}
{{ vulnerability.vulnerability_id }}
{% endif %}

<ul class="list-unstyled mb-0">
{% for alias in aliases %}
<li>
{% if alias|slice:":3" == "CVE" %}
<a href="https://nvd.nist.gov/vuln/detail/{{ alias }}" target="_blank">{{ alias }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
{% elif alias|slice:":4" == "GHSA" %}
<a href="https://github.com/advisories/{{ alias }}" target="_blank">{{ alias }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
{% elif alias|slice:":3" == "NPM" %}
<a href="https://github.com/nodejs/security-wg/blob/main/vuln/npm/{{ alias|slice:"4:" }}.json" target="_blank">{{ alias }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
{% else %}
{{ alias }}
{% endif %}
</li>
{% endfor %}
</ul>
10 changes: 10 additions & 0 deletions scanpipe/templates/scanpipe/includes/vulnerability_summary.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% if vulnerability.summary %}
{% if vulnerability.summary|length > 150 %}
<details>
<summary>{{ vulnerability.summary|slice:":150" }}...</summary>
{{ vulnerability.summary|slice:"150:" }}
</details>
{% else %}
{{ vulnerability.summary }}
{% endif %}
{% endif %}
37 changes: 2 additions & 35 deletions scanpipe/templates/scanpipe/tabset/tab_vulnerabilities.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,10 @@
{% for vulnerability in tab_data.fields.affected_by_vulnerabilities.value %}
<tr>
<td>
<a href="{{ VULNERABLECODE_URL }}/vulnerabilities/{{ vulnerability.vulnerability_id }}" target="_blank">
{{ vulnerability.vulnerability_id }}
<i class="fa-solid fa-up-right-from-square is-small"></i>
</a>
<ul class="list-unstyled mb-0">
{% for alias in aliases %}
<li>
{% if alias|slice:":3" == "CVE" %}
<a href="https://nvd.nist.gov/vuln/detail/{{ alias }}" target="_blank">{{ alias }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
{% elif alias|slice:":4" == "GHSA" %}
<a href="https://github.com/advisories/{{ alias }}" target="_blank">{{ alias }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
{% elif alias|slice:":3" == "NPM" %}
<a href="https://github.com/nodejs/security-wg/blob/main/vuln/npm/{{ alias|slice:"4:" }}.json" target="_blank">{{ alias }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
{% else %}
{{ alias }}
{% endif %}
</li>
{% endfor %}
</ul>
{% include 'scanpipe/includes/vulnerability_id.html' with vulnerability=vulnerability %}
</td>
<td>
{% if vulnerability.summary %}
{% if vulnerability.summary|length > 150 %}
<details>
<summary>{{ vulnerability.summary|slice:":150" }}...</summary>
{{ vulnerability.summary|slice:"150:" }}
</details>
{% else %}
{{ vulnerability.summary }}
{% endif %}
{% endif %}
{% include 'scanpipe/includes/vulnerability_summary.html' with vulnerability=vulnerability only %}
</td>
<td>
{% for key, value in vulnerability.cdx_vulnerability.analysis.items %}
Expand Down
43 changes: 43 additions & 0 deletions scanpipe/templates/scanpipe/vulnerability_list.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{% extends "scanpipe/base.html" %}
{% load humanize %}

{% block title %}ScanCode.io: Vulnerabilities{% endblock %}

{% block content %}
<div id="content-header" class="container is-max-widescreen mb-3">
{% include 'scanpipe/includes/navbar_header.html' %}
<section class="mx-5">
{% include 'scanpipe/includes/breadcrumb.html' with linked_project=True current="Vulnerabilities" %}
<div>{{ object_list|length|intcomma }} results</div>
</section>
</div>

<div class="container is-fluid mb-3">
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth break-word">
{% include 'scanpipe/includes/list_view_thead.html' %}
<tbody>
{% for vulnerability in object_list.values %}
<tr>
<td>
{% include 'scanpipe/includes/vulnerability_id.html' with vulnerability=vulnerability %}
</td>
<td>
{% include 'scanpipe/includes/vulnerability_summary.html' with vulnerability=vulnerability only %}
</td>
<td>
{% for obj in vulnerability.affects %}
{{ obj }}<br>
{% endfor %}
</td>
</tr>
{% empty %}
<tr>
<td colspan="42" class="has-text-centered p-3">
No Vulnerabilities found. <a href="?">Clear search and filters</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% endblock %}
9 changes: 4 additions & 5 deletions scanpipe/tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
1 change: 1 addition & 0 deletions scanpipe/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
27 changes: 26 additions & 1 deletion scanpipe/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 = '<a href="https://vcio//vulnerabilities/VCID-1" target="_blank">'
self.assertContains(response, expected)
expected = '<a href="https://vcio//vulnerabilities/VCID-2" target="_blank">'
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)
Expand Down
5 changes: 5 additions & 0 deletions scanpipe/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,11 @@
views.ProjectMessageListView.as_view(),
name="project_messages",
),
path(
"project/<slug:slug>/vulnerabilities/",
views.VulnerabilityListView.as_view(),
name="project_vulnerabilities",
),
path(
"project/<slug:slug>/archive/",
views.ProjectArchiveView.as_view(),
Expand Down
24 changes: 23 additions & 1 deletion scanpipe/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand Down