-
- {{ vulnerability.vulnerability_id }}
-
-
+
+
+ {{ vulnerability.vulnerability_id }}
+
+
+
+ |
+
+ {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
|
- {{ vulnerability.summary }}
+ {% if vulnerability.min_score %}
+ {{ vulnerability.min_score }} -
+ {% endif %}
+ {% if vulnerability.max_score %}
+
+ {{ vulnerability.max_score }}
+
+ {% endif %}
|
- {% for alias in vulnerability.aliases %}
- {% if alias|slice:":3" == "CVE" %}
- {{ alias }}
-
-
- {% elif alias|slice:":4" == "GHSA" %}
- {{ alias }}
-
-
- {% elif alias|slice:":3" == "NPM" %}
- {{ alias }}
-
-
+ {% if vulnerability.summary %}
+ {% if vulnerability.summary|length > 120 %}
+
+ {{ vulnerability.summary|slice:":120" }}...
+ {{ vulnerability.summary|slice:"120:" }}
+
{% else %}
- {{ alias }}
+ {{ vulnerability.summary }}
{% endif %}
-
- {% endfor %}
+ {% endif %}
|
{% if vulnerability.fixed_packages_html %}
diff --git a/component_catalog/templates/component_catalog/vulnerability_list.html b/component_catalog/templates/component_catalog/vulnerability_list.html
new file mode 100644
index 00000000..ec99b8a0
--- /dev/null
+++ b/component_catalog/templates/component_catalog/vulnerability_list.html
@@ -0,0 +1,23 @@
+{% extends 'object_list_base.html' %}
+{% load i18n %}
+
+{% block page_title %}{% trans "Vulnerabilities" %}{% endblock %}
+
+{% block nav-list-head %}
+ {{ block.super }}
+
+
+
+{% endblock %}
+
+{% block top-right-buttons %}
+ {{ block.super }}
+
+ {{ filter.form.sort }}
+
+{% endblock %}
\ No newline at end of file
diff --git a/component_catalog/tests/__init__.py b/component_catalog/tests/__init__.py
index 8b27d87e..f94e5fc5 100644
--- a/component_catalog/tests/__init__.py
+++ b/component_catalog/tests/__init__.py
@@ -6,22 +6,16 @@
# See https://aboutcode.org for more information about AboutCode FOSS projects.
#
-import random
-import string
-
from component_catalog.models import Component
from component_catalog.models import Package
from component_catalog.models import Vulnerability
-
-
-def make_string(length):
- return "".join(random.choices(string.ascii_letters, k=length))
+from dje.tests import make_string
def make_package(dataspace, package_url=None, is_vulnerable=False, **data):
"""Create a package for test purposes."""
if not package_url and "filename" not in data:
- data["filename"] = make_string(10)
+ data["filename"] = f"package-{make_string(10)}"
package = Package(dataspace=dataspace, **data)
if package_url:
@@ -37,7 +31,7 @@ def make_package(dataspace, package_url=None, is_vulnerable=False, **data):
def make_component(dataspace, is_vulnerable=False, **data):
"""Create a component for test purposes."""
if "name" not in data:
- data["name"] = make_string(10)
+ data["name"] = f"component-{make_string(10)}"
component = Component.objects.create(
dataspace=dataspace,
@@ -53,7 +47,7 @@ def make_component(dataspace, is_vulnerable=False, **data):
def make_vulnerability(dataspace, affecting=None, **data):
"""Create a vulnerability for test purposes."""
if "vulnerability_id" not in data:
- data["vulnerability_id"] = f"VCID-0000-{random.randint(1, 9999):04}"
+ data["vulnerability_id"] = f"VCID-0000-{make_string(4)}"
vulnerability = Vulnerability.objects.create(
dataspace=dataspace,
diff --git a/component_catalog/tests/test_copy.py b/component_catalog/tests/test_copy.py
new file mode 100644
index 00000000..ae7c326a
--- /dev/null
+++ b/component_catalog/tests/test_copy.py
@@ -0,0 +1,55 @@
+#
+# Copyright (c) nexB Inc. and others. All rights reserved.
+# DejaCode is a trademark of nexB Inc.
+# SPDX-License-Identifier: AGPL-3.0-only
+# See https://github.com/aboutcode-org/dejacode for support or download.
+# See https://aboutcode.org for more information about AboutCode FOSS projects.
+#
+
+from django.contrib.contenttypes.models import ContentType
+from django.test import TestCase
+from django.urls import reverse
+
+from component_catalog.models import Package
+from component_catalog.models import PackageAssignedLicense
+from component_catalog.models import Vulnerability
+from component_catalog.tests import make_package
+from dje.models import Dataspace
+from dje.tests import create_superuser
+
+
+class ComponentCatalogCopyTestCase(TestCase):
+ def setUp(self):
+ self.dataspace = Dataspace.objects.create(name="nexB")
+ self.target_dataspace = Dataspace.objects.create(name="Target")
+ self.super_user = create_superuser("super_user", self.dataspace)
+
+ def test_component_catalog_admin_copy_view_vulnerable_package(self):
+ package1 = make_package(self.dataspace, is_vulnerable=True)
+ self.assertEqual(1, package1.affected_by_vulnerabilities.count())
+
+ self.client.login(username=self.super_user.username, password="secret")
+ url = reverse("admin:component_catalog_package_copy")
+ data = {
+ "ids": str(package1.id),
+ "target": self.target_dataspace.id,
+ }
+ response = self.client.get(url, data, follow=True)
+ self.assertContains(response, "Copy the following Packages")
+
+ data = {
+ "ct": str(ContentType.objects.get_for_model(Package).pk),
+ "copy_candidates": package1.id,
+ "source": self.dataspace.id,
+ "targets": self.target_dataspace.id,
+ "form-TOTAL_FORMS": 1,
+ "form-INITIAL_FORMS": 1,
+ "form-0-ct": ContentType.objects.get_for_model(PackageAssignedLicense).pk,
+ }
+ self.client.post(url, data)
+ response = self.client.post(url, data, follow=True)
+ self.assertEqual(200, response.status_code)
+
+ copied_package1 = Package.objects.scope(self.target_dataspace).get()
+ self.assertEqual(0, copied_package1.affected_by_vulnerabilities.count())
+ self.assertEqual(0, Vulnerability.objects.scope(self.target_dataspace).count())
diff --git a/component_catalog/tests/test_filters.py b/component_catalog/tests/test_filters.py
index 1899b7b4..61ee9b76 100644
--- a/component_catalog/tests/test_filters.py
+++ b/component_catalog/tests/test_filters.py
@@ -14,11 +14,13 @@
from component_catalog.filters import ComponentFilterSet
from component_catalog.filters import PackageFilterSet
+from component_catalog.filters import VulnerabilityFilterSet
from component_catalog.models import Component
from component_catalog.models import ComponentKeyword
from component_catalog.models import ComponentType
from component_catalog.tests import make_component
from component_catalog.tests import make_package
+from component_catalog.tests import make_vulnerability
from dje.models import Dataspace
from dje.tests import create_superuser
from dje.tests import create_user
@@ -283,7 +285,7 @@ def test_component_filterset_sort_keeps_default_ordering_from_model(self):
)
-class PackageFilterSearchTestCase(TestCase):
+class PackageFilterSetTestCase(TestCase):
def sorted_results(self, qs):
return sorted([str(package) for package in qs])
@@ -380,9 +382,7 @@ def test_package_filterset_search_match_order_on_purl_fields(self):
self.assertEqual(sorted(expected), self.sorted_results(filterset.qs))
def test_package_filterset_is_vulnerable_filter(self):
- package1 = make_package(
- self.dataspace, package_url="pkg:pypi/django@5.0", is_vulnerable=True
- )
+ package1 = make_package(self.dataspace, is_vulnerable=True)
self.assertTrue(package1.is_vulnerable)
filterset = PackageFilterSet(dataspace=self.dataspace)
@@ -395,3 +395,73 @@ def test_package_filterset_is_vulnerable_filter(self):
data = {"is_vulnerable": "no"}
filterset = PackageFilterSet(dataspace=self.dataspace, data=data)
self.assertNotIn(package1, filterset.qs)
+
+ def test_package_filterset_affected_by_filter(self):
+ package1 = make_package(self.dataspace)
+ package2 = make_package(self.dataspace)
+ vulnerability1 = make_vulnerability(self.dataspace, affecting=package1)
+ self.assertTrue(package1.is_vulnerable)
+ self.assertFalse(package2.is_vulnerable)
+
+ filterset = PackageFilterSet(dataspace=self.dataspace)
+ self.assertIn(package1, filterset.qs)
+ self.assertIn(package2, filterset.qs)
+
+ data = {"affected_by": vulnerability1.vulnerability_id}
+ filterset = PackageFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [package1])
+
+
+class VulnerabilityFilterSetTestCase(TestCase):
+ def setUp(self):
+ self.dataspace = Dataspace.objects.create(name="Reference")
+ self.vulnerability1 = make_vulnerability(self.dataspace, max_score=10.0)
+ self.vulnerability2 = make_vulnerability(
+ self.dataspace, max_score=5.5, aliases=["ALIAS-V2"]
+ )
+ self.vulnerability3 = make_vulnerability(self.dataspace, max_score=2.0)
+ self.vulnerability4 = make_vulnerability(self.dataspace, max_score=None)
+
+ def test_vulnerability_filterset_search(self):
+ data = {"q": self.vulnerability1.vulnerability_id}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [self.vulnerability1])
+
+ data = {"q": "ALIAS-V2"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [self.vulnerability2])
+
+ def test_vulnerability_filterset_sort_nulls_last_ordering(self):
+ data = {"sort": "max_score"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ expected = [
+ self.vulnerability3,
+ self.vulnerability2,
+ self.vulnerability1,
+ self.vulnerability4, # The max_score=None are always last
+ ]
+ self.assertQuerySetEqual(filterset.qs, expected)
+
+ data = {"sort": "-max_score"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ expected = [
+ self.vulnerability1,
+ self.vulnerability2,
+ self.vulnerability3,
+ self.vulnerability4, # The max_score=None are always last
+ ]
+ self.assertQuerySetEqual(filterset.qs, expected)
+
+ def test_vulnerability_filterset_max_score(self):
+ data = {"max_score": "critical"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [self.vulnerability1])
+ data = {"max_score": "high"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [])
+ data = {"max_score": "medium"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [self.vulnerability2])
+ data = {"max_score": "low"}
+ filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data)
+ self.assertQuerySetEqual(filterset.qs, [self.vulnerability3])
diff --git a/component_catalog/tests/test_models.py b/component_catalog/tests/test_models.py
index e942ca93..98ead008 100644
--- a/component_catalog/tests/test_models.py
+++ b/component_catalog/tests/test_models.py
@@ -58,9 +58,7 @@
from license_library.models import LicenseChoice
from license_library.models import LicenseTag
from organization.models import Owner
-from product_portfolio.models import Product
-from product_portfolio.models import ProductComponent
-from product_portfolio.models import ProductPackage
+from product_portfolio.tests import make_product
class ComponentCatalogModelsTestCase(TestCase):
@@ -2490,11 +2488,7 @@ def test_package_create_save_set_usage_policy_from_license(self):
self.assertEqual(package_policy, package5.usage_policy)
def test_component_model_where_used_property(self):
- product1 = Product.objects.create(name="P1", dataspace=self.dataspace)
- ProductComponent.objects.create(
- product=product1, component=self.component1, dataspace=self.dataspace
- )
-
+ make_product(self.dataspace, inventory=[self.component1])
basic_user = create_user("basic_user", self.dataspace)
self.assertEqual("Product 0\n", self.component1.where_used(user=basic_user))
@@ -2502,9 +2496,8 @@ def test_component_model_where_used_property(self):
self.assertEqual("Product 1\n", self.component1.where_used(user=self.user))
def test_package_model_where_used_property(self):
- product1 = Product.objects.create(name="P1", dataspace=self.dataspace)
package1 = Package.objects.create(filename="package", dataspace=self.dataspace)
- ProductPackage.objects.create(product=product1, package=package1, dataspace=self.dataspace)
+ make_product(self.dataspace, inventory=[package1])
basic_user = create_user("basic_user", self.dataspace)
self.assertEqual("Product 0\nComponent 0\n", package1.where_used(user=basic_user))
@@ -2669,6 +2662,18 @@ def test_vulnerability_model_add_affected(self):
self.assertQuerySetEqual(vulnerablity2.affected_packages.all(), [package1])
self.assertQuerySetEqual(vulnerablity2.affected_components.all(), [component1])
+ def test_vulnerability_model_fixed_packages_count_generated_field(self):
+ vulnerablity1 = make_vulnerability(dataspace=self.dataspace)
+ self.assertEqual(0, vulnerablity1.fixed_packages_count)
+
+ vulnerablity1.fixed_packages = [
+ {"purl": "pkg:pypi/gitpython@3.1.41", "is_vulnerable": True},
+ {"purl": "pkg:pypi/gitpython@3.2", "is_vulnerable": False},
+ ]
+ vulnerablity1.save()
+ vulnerablity1.refresh_from_db()
+ self.assertEqual(2, vulnerablity1.fixed_packages_count)
+
def test_vulnerability_model_create_from_data(self):
package1 = make_package(self.dataspace)
vulnerability_data = {
@@ -2700,4 +2705,32 @@ def test_vulnerability_model_create_from_data(self):
self.assertEqual(vulnerability_data["summary"], vulnerability1.summary)
self.assertEqual(vulnerability_data["aliases"], vulnerability1.aliases)
self.assertEqual(vulnerability_data["references"], vulnerability1.references)
+ self.assertEqual(7.5, vulnerability1.min_score)
+ self.assertEqual(7.5, vulnerability1.max_score)
self.assertQuerySetEqual(vulnerability1.affected_packages.all(), [package1])
+
+ def test_vulnerability_model_create_from_data_computed_scores(self):
+ response_file = self.data / "vulnerabilities" / "idna_3.6_response.json"
+ json_data = json.loads(response_file.read_text())
+ affected_by_vulnerabilities = json_data["results"][0]["affected_by_vulnerabilities"]
+ vulnerability1 = Vulnerability.create_from_data(
+ dataspace=self.dataspace,
+ data=affected_by_vulnerabilities[0],
+ )
+ self.assertEqual(2.1, vulnerability1.min_score)
+ self.assertEqual(7.5, vulnerability1.max_score)
+
+ def test_vulnerability_model_queryset_count_methods(self):
+ package1 = make_package(self.dataspace)
+ package2 = make_package(self.dataspace)
+ vulnerablity1 = make_vulnerability(dataspace=self.dataspace)
+ vulnerablity1.add_affected([package1, package2])
+ make_product(self.dataspace, inventory=[package1, package2])
+
+ qs = (
+ Vulnerability.objects.scope(self.dataspace)
+ .with_affected_products_count()
+ .with_affected_packages_count()
+ )
+ self.assertEqual(2, qs[0].affected_packages_count)
+ self.assertEqual(1, qs[0].affected_products_count)
diff --git a/component_catalog/tests/test_views.py b/component_catalog/tests/test_views.py
index e6b21573..7e3f31d8 100644
--- a/component_catalog/tests/test_views.py
+++ b/component_catalog/tests/test_views.py
@@ -39,6 +39,9 @@
from component_catalog.models import ComponentType
from component_catalog.models import Package
from component_catalog.models import Subcomponent
+from component_catalog.models import Vulnerability
+from component_catalog.tests import make_component
+from component_catalog.tests import make_package
from component_catalog.tests import make_vulnerability
from component_catalog.views import ComponentAddView
from component_catalog.views import ComponentListView
@@ -48,6 +51,7 @@
from dejacode_toolkit.vulnerablecode import get_plain_purls
from dje.copier import copy_object
from dje.models import Dataspace
+from dje.models import DataspaceConfiguration
from dje.models import ExternalReference
from dje.models import ExternalSource
from dje.models import History
@@ -4811,3 +4815,63 @@ def test_anonymous_user_cannot_access_reference_data(self):
self.assertEqual(404, self.client.get(self.d1c2.get_absolute_url()).status_code)
self.assertEqual(200, self.client.get(self.dmc1.get_absolute_url()).status_code)
self.assertEqual(200, self.client.get(self.dmc2.get_absolute_url()).status_code)
+
+
+class VulnerabilityViewsTestCase(TestCase):
+ def setUp(self):
+ self.dataspace = Dataspace.objects.create(
+ name="Dataspace",
+ enable_vulnerablecodedb_access=True,
+ )
+ DataspaceConfiguration.objects.create(
+ dataspace=self.dataspace,
+ vulnerablecode_url="vulnerablecode_url/",
+ )
+ self.super_user = create_superuser("super_user", self.dataspace)
+
+ self.component1 = make_component(self.dataspace)
+ self.component2 = make_component(self.dataspace)
+ self.package1 = make_package(self.dataspace)
+ self.package2 = make_package(self.dataspace)
+ self.vulnerability_p1 = make_vulnerability(self.dataspace, affecting=self.component1)
+ self.vulnerability_c1 = make_vulnerability(self.dataspace, affecting=self.package1)
+ self.vulnerability1 = make_vulnerability(self.dataspace)
+
+ def test_vulnerability_list_view_num_queries(self):
+ self.client.login(username=self.super_user.username, password="secret")
+ with self.assertNumQueries(8):
+ response = self.client.get(reverse("component_catalog:vulnerability_list"))
+
+ vulnerability_count = Vulnerability.objects.count()
+ expected = f'{vulnerability_count} results'
+ self.assertContains(response, expected, html=True)
+
+ def test_vulnerability_list_view_enable_vulnerablecodedb_access(self):
+ self.client.login(username=self.super_user.username, password="secret")
+ vulnerability_list_url = reverse("component_catalog:vulnerability_list")
+ response = self.client.get(vulnerability_list_url)
+ self.assertEqual(200, response.status_code)
+ vulnerability_header_link = (
+ f''
+ )
+ self.assertContains(response, vulnerability_header_link)
+
+ self.dataspace.enable_vulnerablecodedb_access = False
+ self.dataspace.save()
+ response = self.client.get(reverse("component_catalog:vulnerability_list"))
+ self.assertEqual(404, response.status_code)
+
+ response = self.client.get(reverse("component_catalog:package_list"))
+ self.assertNotContains(response, vulnerability_header_link)
+
+ def test_vulnerability_list_view_vulnerability_id_link(self):
+ self.client.login(username=self.super_user.username, password="secret")
+ response = self.client.get(reverse("component_catalog:vulnerability_list"))
+ expected = f"""
+
+ {self.vulnerability1.vulnerability_id}
+
+
+ """
+ self.assertContains(response, expected, html=True)
diff --git a/component_catalog/urls.py b/component_catalog/urls.py
index 53ff217a..50e87277 100644
--- a/component_catalog/urls.py
+++ b/component_catalog/urls.py
@@ -21,6 +21,7 @@
from component_catalog.views import PackageTabScanView
from component_catalog.views import PackageUpdateView
from component_catalog.views import ScanListView
+from component_catalog.views import VulnerabilityListView
from component_catalog.views import component_create_ajax_view
from component_catalog.views import delete_scan_view
from component_catalog.views import package_create_ajax_view
@@ -148,6 +149,14 @@
),
]
+vulnerabilities_patterns = [
+ path(
+ "vulnerabilities/",
+ VulnerabilityListView.as_view(),
+ name="vulnerability_list",
+ ),
+]
+
# WARNING: we moved the components/ patterns from the include to the following
# since we need the packages/ and scans/ to be register on the root "/"
@@ -239,4 +248,4 @@ def component_path(path_segment, view):
]
-urlpatterns = packages_patterns + component_patterns + scans_patterns
+urlpatterns = packages_patterns + component_patterns + scans_patterns + vulnerabilities_patterns
diff --git a/component_catalog/views.py b/component_catalog/views.py
index 22aff7de..aa703af2 100644
--- a/component_catalog/views.py
+++ b/component_catalog/views.py
@@ -22,6 +22,7 @@
from django.core import signing
from django.core.validators import EMPTY_VALUES
from django.db.models import Count
+from django.db.models import F
from django.db.models import Prefetch
from django.http import FileResponse
from django.http import Http404
@@ -52,6 +53,7 @@
from component_catalog.filters import ComponentFilterSet
from component_catalog.filters import PackageFilterSet
+from component_catalog.filters import VulnerabilityFilterSet
from component_catalog.forms import AddMultipleToComponentForm
from component_catalog.forms import AddToComponentForm
from component_catalog.forms import AddToProductAdminForm
@@ -71,6 +73,7 @@
from component_catalog.models import Package
from component_catalog.models import PackageAlreadyExistsWarning
from component_catalog.models import Subcomponent
+from component_catalog.models import Vulnerability
from dejacode_toolkit.download import DataCollectionException
from dejacode_toolkit.purldb import PurlDB
from dejacode_toolkit.scancodeio import ScanCodeIO
@@ -1492,13 +1495,13 @@ def package_create_ajax_view(request):
errors.append(str(error))
redirect_url = reverse("component_catalog:package_list")
- len_created = len(created)
scancodeio = ScanCodeIO(dataspace)
- if scancodeio.is_configured() and dataspace.enable_package_scanning:
+ download_urls = [package.download_url for package in created if package.download_url]
+ if download_urls and dataspace.enable_package_scanning and scancodeio.is_configured():
# The availability of the each `download_url` is checked in the task.
tasks.scancodeio_submit_scan.delay(
- uris=[package.download_url for package in created if package.download_url],
+ uris=download_urls,
user_uuid=user.uuid,
dataspace_uuid=dataspace.uuid,
)
@@ -1508,6 +1511,7 @@ def package_create_ajax_view(request):
for package in created:
package.fetch_vulnerabilities()
+ len_created = len(created)
if len_created == 1:
redirect_url = created[0].get_absolute_url()
messages.success(request, "The Package was successfully created.")
@@ -1516,6 +1520,17 @@ def package_create_ajax_view(request):
msg = f"The following Packages were successfully created{scan_msg}:\n{packages}"
messages.success(request, format_html(msg))
+ purls_wihtout_download_url = [package for package in created if not package.download_url]
+ if purls_wihtout_download_url:
+ packages = [package.get_absolute_link() for package in purls_wihtout_download_url]
+ msg = (
+ "Unable to determine a download URL for the following packages."
+ " A download URL is required to fetch and scan a package from DejaCode."
+ "\nAlternatively, you can directly provide a download URL instead of a "
+ "package URL to create the packages.\n"
+ )
+ messages.warning(request, format_html(msg + "\n".join(packages)))
+
if errors:
messages.error(request, format_html("\n".join(errors)))
if warnings:
@@ -2464,3 +2479,57 @@ def get_tab_fields(self):
tab_fields.extend(get_purldb_tab_fields(purldb_entry, user.dataspace))
return {"fields": tab_fields}
+
+
+class VulnerabilityListView(
+ LoginRequiredMixin,
+ DataspacedFilterView,
+):
+ model = Vulnerability
+ filterset_class = VulnerabilityFilterSet
+ template_name = "component_catalog/vulnerability_list.html"
+ template_list_table = "component_catalog/tables/vulnerability_list_table.html"
+ table_headers = (
+ Header("vulnerability_id", _("Vulnerability")),
+ Header("aliases", _("Aliases")),
+ # Keep `max_score` to enable column sorting
+ Header("max_score", _("Score"), help_text="Severity score range", filter="max_score"),
+ Header("summary", _("Summary")),
+ Header("affected_products_count", _("Affected products"), help_text="Affected products"),
+ Header("affected_packages_count", _("Affected packages"), help_text="Affected packages"),
+ Header("fixed_packages_count", _("Fixed by"), help_text="Fixed by packages"),
+ )
+
+ def get_queryset(self):
+ return (
+ super()
+ .get_queryset()
+ .only(
+ "uuid",
+ "vulnerability_id",
+ "aliases",
+ "summary",
+ "fixed_packages_count",
+ "max_score",
+ "min_score",
+ "created_date",
+ "last_modified_date",
+ "dataspace",
+ )
+ .with_affected_products_count()
+ .with_affected_packages_count()
+ .order_by(
+ F("max_score").desc(nulls_last=True),
+ "-min_score",
+ )
+ )
+
+ def get_context_data(self, **kwargs):
+ context_data = super().get_context_data(**kwargs)
+
+ if not self.dataspace.enable_vulnerablecodedb_access:
+ raise Http404("VulnerableCode access is not enabled.")
+
+ vulnerablecode = VulnerableCode(self.dataspace)
+ context_data["vulnerablecode_url"] = vulnerablecode.service_url
+ return context_data
diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css
index 26bf7b52..957dcdd4 100644
--- a/dejacode/static/css/dejacode_bootstrap.css
+++ b/dejacode/static/css/dejacode_bootstrap.css
@@ -358,6 +358,55 @@ table.packages-table .column-primary_language {
margin-right: 0.3rem;
}
+/* -- Vulnerability List -- */
+table.vulnerabilities-table .column-vulnerability_id {
+ width: 220px;
+}
+table.vulnerabilities-table .column-aliases {
+ width: 210px;
+}
+table.vulnerabilities-table .column-score_range {
+ width: 110px;
+}
+table.vulnerabilities-table .column-priority {
+ width: 110px;
+}
+table.vulnerabilities-table .column-summary {
+ width: 300px;
+ max-width: 300px;
+}
+
+/* -- Vulnerability tab -- */
+#tab_vulnerabilities .column-vulnerability_id {
+ width: 210px;
+}
+#tab_vulnerabilities .column-aliases {
+ width: 210px;
+}
+#tab_vulnerabilities .column-max_score {
+ width: 105px;
+}
+#tab_vulnerabilities .column-column-affected_packages {
+ width: 320px;
+}
+
+/* -- Dependency tab -- */
+#tab_dependencies .column-for_package {
+ width: 250px;
+}
+#tab_dependencies .column-resolved_to_package {
+ width: 250px;
+}
+#tab_dependencies .column-is_runtime {
+ width: 100px;
+}
+#tab_dependencies .column-column-is_optional {
+ width: 100px;
+}
+#tab_dependencies .column-column-is_resolved {
+ width: 88px;
+}
+
/* -- Package Details -- */
textarea.licenseexpressionwidget {
height: 62px;
diff --git a/dje/copier.py b/dje/copier.py
index 758c23cb..385d510c 100644
--- a/dje/copier.py
+++ b/dje/copier.py
@@ -65,6 +65,7 @@
"scanned_by",
"project_uuid",
"default_assignee",
+ "affected_by_vulnerabilities",
]
@@ -199,6 +200,11 @@ def copy_to(reference_obj, target_dataspace, user, **kwargs):
field = reference_obj._meta.get_field(field_name)
except FieldDoesNotExist:
continue
+
+ # Simply skip many_to_many fields.
+ if field.many_to_many:
+ continue
+
# get_default Return None or empty string (depending on the Field type)
# if no default value was declared.
setattr(target_obj, field_name, field.get_default())
@@ -306,6 +312,9 @@ def copy_m2m_fields(reference_obj, target_dataspace, user, update=False, **kwarg
m2m_fields = reference_obj._meta.many_to_many
for m2m_field in m2m_fields:
+ if m2m_field.name in ALWAYS_EXCLUDE:
+ continue
+
through_model = m2m_field.remote_field.through # Relation Model (through table)
m2m_field_name = m2m_field.m2m_field_name() # FK fields names on the Relation Model
reference_m2m_qs = through_model.objects.filter(**{m2m_field_name: reference_obj})
diff --git a/dje/filters.py b/dje/filters.py
index 85bbb3ce..0aab8a31 100644
--- a/dje/filters.py
+++ b/dje/filters.py
@@ -55,7 +55,10 @@ def is_active(self):
)
def get_query_no_sort(self):
- return remove_field_from_query_dict(self.data, "sort")
+ sort_field_name = "sort"
+ if self.form_prefix:
+ sort_field_name = f"{self.form_prefix}-{sort_field_name}"
+ return remove_field_from_query_dict(self.data, sort_field_name)
def get_filter_breadcrumb(self, field_name, data_field_name, value):
return {
@@ -86,6 +89,7 @@ def __init__(self, *args, **kwargs):
self.dynamic_qs = kwargs.pop("dynamic_qs", True)
self.parent_qs_cache = {}
+ self.anchor = kwargs.pop("anchor", None)
super().__init__(*args, **kwargs)
diff --git a/dje/forms.py b/dje/forms.py
index 0694cf09..460bb957 100644
--- a/dje/forms.py
+++ b/dje/forms.py
@@ -435,7 +435,12 @@ def __init__(self, user, *args, **kwargs):
self.model_class = ContentType.objects.get(pk=ct).model_class()
- exclude_choices = self.model_class.get_exclude_choices()
+ # Some implicitly created models, such as some Many2Many, do not inherit the
+ # get_exclude_choices method from the DataspacedModel class.
+ exclude_choices = []
+ if hasattr(self.model_class, "get_exclude_choices"):
+ exclude_choices = self.model_class.get_exclude_choices()
+
self.fields["exclude_copy"].choices = exclude_choices
self.fields["exclude_update"].choices = exclude_choices
diff --git a/dje/templates/admin/base_site.html b/dje/templates/admin/base_site.html
index 8c7ca25a..ae0db44c 100644
--- a/dje/templates/admin/base_site.html
+++ b/dje/templates/admin/base_site.html
@@ -42,6 +42,7 @@
{% url 'workflow:request_list' as request_list_url %}
{% url 'component_catalog:scan_list' as scan_list_url %}
{% url 'purldb:purldb_list' as purldb_list_url %}
+ {% url 'component_catalog:vulnerability_list' as vulnerability_list_url %}
{% url 'api_v2:api-root' as api_root_url %}
{% if report_list_url or request_list_url or api_root_url %}
@@ -64,6 +65,10 @@
{% trans 'PurlDB' %}
{% endif %}
{% endif %}
+ {% if user.dataspace.enable_vulnerablecodedb_access %}
+
+ {% trans 'Vulnerabilities' %}
+ {% endif %}
{% if api_root_url %}
{% trans 'API Root' %}
diff --git a/dje/templates/global_search.html b/dje/templates/global_search.html
index 567cdec4..880e828e 100644
--- a/dje/templates/global_search.html
+++ b/dje/templates/global_search.html
@@ -140,7 +140,7 @@
{% block javascripts %}
{{ block.super }}
-
+
{% if include_purldb %}
+
{% endblock %}
\ No newline at end of file
diff --git a/dje/templates/object_details_base.html b/dje/templates/object_details_base.html
index 750db7ad..15d37841 100644
--- a/dje/templates/object_details_base.html
+++ b/dje/templates/object_details_base.html
@@ -77,8 +77,8 @@ |