Skip to content

Commit

Permalink
Add a Vulnerabilities tab in the Product details view #95
Browse files Browse the repository at this point in the history
Signed-off-by: tdruez <tdruez@nexb.com>
  • Loading branch information
tdruez committed Aug 28, 2024
1 parent 08f9367 commit bb91c1b
Show file tree
Hide file tree
Showing 6 changed files with 226 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<ul class="list-unstyled">
<ul class="list-unstyled mb-0">
{% for alias in aliases %}
<li>
{% if alias|slice:":3" == "CVE" %}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,21 @@
{% trans 'Affected by' %}
</span>
</th>
<th>
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Summary of the vulnerability.">
{% trans 'Summary' %}
</span>
</th>
<th style="width: 210px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="A list of aliases for this vulnerability.">
{% trans 'Aliases' %}
</span>
</th>
<th style="width: 90px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Severity score range.">
{% trans 'Score' %}
</span>
</th>
<th>
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Summary of the vulnerability.">
{% trans 'Summary' %}
</span>
</th>
<th style="min-width: 320px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="The identifiers of Package Versions that have been reported to fix a specific vulnerability and collected in VulnerableCodeDB.">
{% trans 'Fixed packages' %}
Expand All @@ -28,16 +33,37 @@
{% for vulnerability in values.vulnerabilities %}
<tr>
<td>
<a href="{{ values.vulnerablecode_url }}vulnerabilities/{{ vulnerability.vulnerability_id }}" target="_blank">
{{ vulnerability.vulnerability_id }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
<strong>
<a href="{{ values.vulnerablecode_url }}vulnerabilities/{{ vulnerability.vulnerability_id }}" target="_blank">
{{ vulnerability.vulnerability_id }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
</strong>
</td>
<td>
{{ vulnerability.summary }}
{% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
</td>
<td>
{% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
{% if vulnerability.min_score %}
{{ vulnerability.min_score }} -
{% endif %}
{% if vulnerability.max_score %}
<strong>
{{ vulnerability.max_score }}
</strong>
{% endif %}
</td>
<td>
{% if vulnerability.summary %}
{% if vulnerability.summary|length > 120 %}
<details>
<summary>{{ vulnerability.summary|slice:":120" }}...</summary>
{{ vulnerability.summary|slice:"120:" }}
</details>
{% else %}
{{ vulnerability.summary }}
{% endif %}
{% endif %}
</td>
<td>
{% if vulnerability.fixed_packages_html %}
Expand Down
5 changes: 5 additions & 0 deletions product_portfolio/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from component_catalog.models import KeywordsMixin
from component_catalog.models import LicenseExpressionMixin
from component_catalog.models import Package
from component_catalog.models import Vulnerability
from component_catalog.models import component_mixin_factory
from component_catalog.vulnerabilities import fetch_for_queryset
from dje import tasks
Expand Down Expand Up @@ -495,6 +496,10 @@ def fetch_vulnerabilities(self):
"""Fetch and update the vulnerabilties of all the Package of this Product."""
return fetch_for_queryset(self.all_packages, self.dataspace)

def get_vulnerability_qs(self):
"""Return a QuerySet of all Vulnerability instances related to this product"""
return Vulnerability.objects.filter(affected_packages__in=self.packages.all())


class ProductRelationStatus(BaseStatusMixin, DataspacedModel):
class Meta(BaseStatusMixin.Meta):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
{% load i18n %}
<table class="table table-bordered table-hover table-md text-break">
<thead>
<tr>
<th style="width: 210px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Link to the VulnerableCode app.">
{% trans 'Affected by' %}
</span>
</th>
<th style="width: 210px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="A list of aliases for this vulnerability.">
{% trans 'Aliases' %}
</span>
</th>
<th style="width: 90px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Severity score range.">
{% trans 'Score' %}
</span>
</th>
<th>
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Summary of the vulnerability.">
{% trans 'Summary' %}
</span>
</th>
<th style="min-width: 320px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Affected packages.">
{% trans 'Affected packages' %}
</span>
</th>
</tr>
</thead>
<tbody>
{% for vulnerability in page_obj.object_list %}
<tr>
<td>
<strong>
<a href="{{ values.vulnerablecode_url }}vulnerabilities/{{ vulnerability.vulnerability_id }}" target="_blank">
{{ vulnerability.vulnerability_id }}
<i class="fa-solid fa-up-right-from-square mini"></i>
</a>
</strong>
</td>
<td>
{% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
</td>
<td>
{% if vulnerability.min_score %}
{{ vulnerability.min_score }} -
{% endif %}
{% if vulnerability.max_score %}
<strong>
{{ vulnerability.max_score }}
</strong>
{% endif %}
</td>
<td>
{% if vulnerability.summary %}
{% if vulnerability.summary|length > 120 %}
<details>
<summary>{{ vulnerability.summary|slice:":120" }}...</summary>
{{ vulnerability.summary|slice:"120:" }}
</details>
{% else %}
{{ vulnerability.summary }}
{% endif %}
{% endif %}
</td>
<td>
<ul class="list-unstyled mb-0">
{% for package in vulnerability.affected_packages.all %}
<li>
<a href="{{ package.get_absolute_url }}#vulnerabilities" target="_blank">{{ package }}</a>
</li>
{% endfor %}
</ul>
</td>
</tr>
{% endfor %}
</tbody>
</table>
2 changes: 2 additions & 0 deletions product_portfolio/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from product_portfolio.views import ProductTabDependenciesView
from product_portfolio.views import ProductTabImportsView
from product_portfolio.views import ProductTabInventoryView
from product_portfolio.views import ProductTabVulnerabilitiesView
from product_portfolio.views import ProductTreeComparisonView
from product_portfolio.views import ProductUpdateView
from product_portfolio.views import PullProjectDataFromScanCodeIOView
Expand Down Expand Up @@ -94,6 +95,7 @@ def product_path(path_segment, view):
*product_path("import_manifests", ImportManifestsView.as_view()),
*product_path("tab_codebase", ProductTabCodebaseView.as_view()),
*product_path("tab_dependencies", ProductTabDependenciesView.as_view()),
*product_path("tab_vulnerabilities", ProductTabVulnerabilitiesView.as_view()),
*product_path("tab_imports", ProductTabImportsView.as_view()),
*product_path("tab_inventory", ProductTabInventoryView.as_view()),
*product_path("pull_project_data", PullProjectDataFromScanCodeIOView.as_view()),
Expand Down
105 changes: 101 additions & 4 deletions product_portfolio/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from django.core.paginator import Paginator
from django.db import transaction
from django.db.models import Count
from django.db.models import F
from django.db.models import Prefetch
from django.db.models.functions import Lower
from django.forms import modelformset_factory
Expand Down Expand Up @@ -53,6 +54,7 @@
from openpyxl.styles import NamedStyle
from openpyxl.styles import Side

from component_catalog.filters import VulnerabilityFilterSet
from component_catalog.forms import ComponentAjaxForm
from component_catalog.license_expression_dje import build_licensing
from component_catalog.license_expression_dje import parse_expression
Expand All @@ -64,6 +66,7 @@
from dejacode_toolkit.scancodeio import get_package_download_url
from dejacode_toolkit.scancodeio import get_scan_results_as_file_url
from dejacode_toolkit.utils import sha1
from dejacode_toolkit.vulnerablecode import VulnerableCode
from dje import tasks
from dje.client_data import add_client_data
from dje.filters import BooleanChoiceFilter
Expand Down Expand Up @@ -285,6 +288,7 @@ class ProductDetailsView(
"owner",
],
},
"vulnerabilities": {},
"dependencies": {
"fields": [
"dependencies",
Expand Down Expand Up @@ -514,6 +518,41 @@ def tab_dependencies(self):
"fields": [(None, tab_context, None, template)],
}

def tab_vulnerabilities(self):
dataspace = self.object.dataspace
vulnerablecode = VulnerableCode(self.object.dataspace)
display_tab_contions = [
dataspace.enable_vulnerablecodedb_access,
vulnerablecode.is_configured(),
]
if not all(display_tab_contions):
return

vulnerability_qs = self.object.get_vulnerability_qs()
vulnerability_count = vulnerability_qs.count()
if not vulnerability_count: # TODO: Display tab as disabled instead
return

label = (
f'Vulnerabilities <span class="badge badge-vulnerability">{vulnerability_count}</span>'
)

# Pass the current request query context to the async request
tab_view_url = self.object.get_url("tab_vulnerabilities")
if full_query_string := self.request.META["QUERY_STRING"]:
tab_view_url += f"?{full_query_string}"

template = "tabs/tab_async_loader.html"
tab_context = {
"tab_view_url": tab_view_url,
"tab_object_name": "vulnerabilities",
}

return {
"label": format_html(label),
"fields": [(None, tab_context, None, template)],
}

def tab_codebase(self):
codebaseresources_count = self.object.codebaseresources.count()
if not codebaseresources_count:
Expand Down Expand Up @@ -698,7 +737,7 @@ def get_context_data(self, **kwargs):
self.request.GET,
queryset=productpackage_qs,
dataspace=self.object.dataspace,
prefix="inventory",
prefix=self.tab_id,
anchor="#inventory",
)

Expand Down Expand Up @@ -726,7 +765,7 @@ def get_context_data(self, **kwargs):
self.request.GET,
queryset=productcomponent_qs,
dataspace=self.object.dataspace,
prefix="inventory",
prefix=self.tab_id,
anchor="#inventory",
)

Expand Down Expand Up @@ -893,7 +932,7 @@ def get_context_data(self, **kwargs):
self.request.GET,
queryset=codebaseresource_qs,
dataspace=self.object.dataspace,
prefix="codebase",
prefix=self.tab_id,
)

paginator = Paginator(filter_codebaseresource.qs, self.paginate_by)
Expand Down Expand Up @@ -982,7 +1021,7 @@ def get_context_data(self, **kwargs):
self.request.GET,
queryset=dependency_qs,
dataspace=product.dataspace,
prefix="dependencies",
prefix=self.tab_id,
)

filtered_and_ordered_qs = filter_dependency.qs.order_by(
Expand Down Expand Up @@ -1025,6 +1064,64 @@ def get_context_data(self, **kwargs):
return context_data


class ProductTabVulnerabilitiesView(
LoginRequiredMixin,
BaseProductView,
PreviousNextPaginationMixin,
TabContentView,
):
# TODO: Remove duplication
template_name = "product_portfolio/tabs/tab_vulnerabilities.html"
paginate_by = 50
query_dict_page_param = "vulnerabilities-page"
tab_id = "vulnerabilities"

def get_context_data(self, **kwargs):
context_data = super().get_context_data(**kwargs)
product = self.object
base_vulnerability_qs = product.get_vulnerability_qs()
total_count = base_vulnerability_qs.count()

package_qs = Package.objects.filter(product=product).only_rendering_fields()
vulnerability_qs = base_vulnerability_qs.prefetch_related(
Prefetch("affected_packages", package_qs)
).order_by(
F("max_score").desc(nulls_last=True),
"-min_score",
)

filter_vulnerability = VulnerabilityFilterSet(
self.request.GET,
queryset=vulnerability_qs,
dataspace=product.dataspace,
prefix=self.tab_id,
)

paginator = Paginator(filter_vulnerability.qs, self.paginate_by)
page_number = self.request.GET.get(self.query_dict_page_param)
page_obj = paginator.get_page(page_number)

context_data.update(
{
"filter_vulnerability": filter_vulnerability,
"page_obj": page_obj,
"total_count": total_count,
"search_query": self.request.GET.get("vulnerabilities-q", ""),
}
)

if page_obj:
previous_url, next_url = self.get_previous_next(page_obj)
context_data.update(
{
"previous_url": (previous_url or "") + f"#{self.tab_id}",
"next_url": (next_url or "") + f"#{self.tab_id}",
}
)

return context_data


class ProductTabImportsView(
LoginRequiredMixin,
BaseProductView,
Expand Down

0 comments on commit bb91c1b

Please sign in to comment.