-
- {{ vulnerability.vulnerability_id }}
-
-
+
+
+ {{ vulnerability.vulnerability_id }}
+
+
+
|
- {{ vulnerability.summary }}
+ {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
|
- {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
+ {% if vulnerability.min_score %}
+ {{ vulnerability.min_score }} -
+ {% endif %}
+ {% if vulnerability.max_score %}
+
+ {{ vulnerability.max_score }}
+
+ {% endif %}
+ |
+
+ {% if vulnerability.summary %}
+ {% if vulnerability.summary|length > 120 %}
+
+ {{ vulnerability.summary|slice:":120" }}...
+ {{ vulnerability.summary|slice:"120:" }}
+
+ {% else %}
+ {{ vulnerability.summary }}
+ {% endif %}
+ {% endif %}
|
{% if vulnerability.fixed_packages_html %}
diff --git a/product_portfolio/models.py b/product_portfolio/models.py
index 778e6ab6..09255964 100644
--- a/product_portfolio/models.py
+++ b/product_portfolio/models.py
@@ -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
@@ -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):
diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html
new file mode 100644
index 00000000..864ef87d
--- /dev/null
+++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html
@@ -0,0 +1,80 @@
+{% load i18n %}
+
+
+
+
+
+ {% trans 'Affected by' %}
+
+ |
+
+
+ {% trans 'Aliases' %}
+
+ |
+
+
+ {% trans 'Score' %}
+
+ |
+
+
+ {% trans 'Summary' %}
+
+ |
+
+
+ {% trans 'Affected packages' %}
+
+ |
+
+
+
+ {% for vulnerability in page_obj.object_list %}
+
+
+
+
+ {{ vulnerability.vulnerability_id }}
+
+
+
+ |
+
+ {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
+ |
+
+ {% if vulnerability.min_score %}
+ {{ vulnerability.min_score }} -
+ {% endif %}
+ {% if vulnerability.max_score %}
+
+ {{ vulnerability.max_score }}
+
+ {% endif %}
+ |
+
+ {% if vulnerability.summary %}
+ {% if vulnerability.summary|length > 120 %}
+
+ {{ vulnerability.summary|slice:":120" }}...
+ {{ vulnerability.summary|slice:"120:" }}
+
+ {% else %}
+ {{ vulnerability.summary }}
+ {% endif %}
+ {% endif %}
+ |
+
+
+ {% for package in vulnerability.affected_packages.all %}
+ -
+ {{ package }}
+
+ {% endfor %}
+
+ |
+
+ {% endfor %}
+
+
\ No newline at end of file
diff --git a/product_portfolio/urls.py b/product_portfolio/urls.py
index 6eb41237..ff4814b9 100644
--- a/product_portfolio/urls.py
+++ b/product_portfolio/urls.py
@@ -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
@@ -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()),
diff --git a/product_portfolio/views.py b/product_portfolio/views.py
index 81f22f54..f361d030 100644
--- a/product_portfolio/views.py
+++ b/product_portfolio/views.py
@@ -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
@@ -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
@@ -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
@@ -285,6 +288,7 @@ class ProductDetailsView(
"owner",
],
},
+ "vulnerabilities": {},
"dependencies": {
"fields": [
"dependencies",
@@ -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 {vulnerability_count}'
+ )
+
+ # 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:
@@ -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",
)
@@ -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",
)
@@ -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)
@@ -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(
@@ -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,
|