diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 0c69f806..cb2d070a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -6,6 +6,11 @@ Release notes - Rename ProductDependency is_resolved to is_pinned. https://github.com/aboutcode-org/dejacode/issues/189 +- Add new fields on the Vulnerability model: `exploitability`, `weighted_severity`, + `risk_score`. The field are displayed in all relevant part of the UI where + vulnerability data is available. + https://github.com/aboutcode-org/dejacode/issues/98 + ### Version 5.2.1 - Fix the models documentation navigation. diff --git a/component_catalog/importers.py b/component_catalog/importers.py index be05c3ae..ca588f75 100644 --- a/component_catalog/importers.py +++ b/component_catalog/importers.py @@ -41,7 +41,7 @@ from policy.models import UsagePolicy from product_portfolio.models import ProductComponent from product_portfolio.models import ProductPackage -from vulnerabilities.fetch import fetch_for_queryset +from vulnerabilities.fetch import fetch_for_packages keywords_help = ( get_help_text(Component, "keywords") @@ -433,7 +433,7 @@ def save_all(self): if self.dataspace.enable_vulnerablecodedb_access: package_pks = [package.pk for package in self.results["added"]] package_qs = Package.objects.scope(dataspace=self.dataspace).filter(pk__in=package_pks) - fetch_for_queryset(package_qs, self.dataspace) + fetch_for_packages(package_qs, self.dataspace) class SubcomponentImportForm( diff --git a/component_catalog/migrations/0010_component_risk_score_package_risk_score.py b/component_catalog/migrations/0010_component_risk_score_package_risk_score.py new file mode 100644 index 00000000..5935333d --- /dev/null +++ b/component_catalog/migrations/0010_component_risk_score_package_risk_score.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.9 on 2024-11-18 10:01 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0009_componentaffectedbyvulnerability_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='component', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.0 and 10.0, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True), + ), + migrations.AddField( + model_name='package', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.0 and 10.0, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True), + ), + ] diff --git a/component_catalog/models.py b/component_catalog/models.py index 9c60e9c2..35b46eb7 100644 --- a/component_catalog/models.py +++ b/component_catalog/models.py @@ -1675,6 +1675,7 @@ def only_rendering_fields(self): *PACKAGE_URL_FIELDS, "filename", "license_expression", + "risk_score", "dataspace__name", "dataspace__show_usage_policy_in_user_views", ) diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 77b010f8..9fa439bd 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -1,4 +1,14 @@ {% load i18n %} +
| - - {% trans 'Aliases' %} + | + + {% trans 'Summary' %} | -- - {% trans 'Score' %} + | + + {% trans 'Exploitability' %} | -- - {% trans 'Summary' %} + | + + {% trans 'Severity' %} + + | ++ + {% trans 'Risk' %} | @@ -43,19 +58,9 @@ {{ vulnerability.vulnerability_id }} {% endif %} - - | - {% 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 %}
+
+ {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
+
|
{% if vulnerability.summary %} @@ -69,6 +74,15 @@ {% endif %} {% endif %} | ++ {% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %} + | ++ {{ vulnerability.weighted_severity|default_if_none:"" }} + | ++ {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + |
{% if vulnerability.fixed_packages_html %}
{{ vulnerability.fixed_packages_html }}
diff --git a/component_catalog/tests/test_importers.py b/component_catalog/tests/test_importers.py
index fefb5412..9f24e251 100644
--- a/component_catalog/tests/test_importers.py
+++ b/component_catalog/tests/test_importers.py
@@ -1397,9 +1397,9 @@ def test_package_import_add_to_product(self):
self.assertContains(response, expected3)
self.assertContains(response, expected4)
- @mock.patch("component_catalog.importers.fetch_for_queryset")
- def test_package_import_fetch_vulnerabilities(self, mock_fetch_for_queryset):
- mock_fetch_for_queryset.return_value = None
+ @mock.patch("component_catalog.importers.fetch_for_packages")
+ def test_package_import_fetch_vulnerabilities(self, mock_fetch_for_packages):
+ mock_fetch_for_packages.return_value = None
self.dataspace.enable_vulnerablecodedb_access = True
self.dataspace.save()
@@ -1407,7 +1407,7 @@ def test_package_import_fetch_vulnerabilities(self, mock_fetch_for_queryset):
importer = PackageImporter(self.super_user, file)
importer.save_all()
self.assertEqual(2, len(importer.results["added"]))
- mock_fetch_for_queryset.assert_called()
+ mock_fetch_for_packages.assert_called()
class SubcomponentImporterTestCase(TestCase):
diff --git a/component_catalog/tests/test_models.py b/component_catalog/tests/test_models.py
index 5f7b2cbb..fbbbb5fb 100644
--- a/component_catalog/tests/test_models.py
+++ b/component_catalog/tests/test_models.py
@@ -1683,6 +1683,10 @@ def test_package_model_update_from_data(self):
package.refresh_from_db()
self.assertEqual(new_data["filename"], package.filename)
+ new_data = {"filename": "new_filename2"}
+ updated_fields = package.update_from_data(user=None, data=new_data, override=True)
+ self.assertEqual(["filename"], updated_fields)
+
@mock.patch("component_catalog.models.collect_package_data")
def test_package_model_create_from_url(self, mock_collect):
self.assertIsNone(Package.create_from_url(url=" ", user=self.user))
diff --git a/component_catalog/views.py b/component_catalog/views.py
index 05bca001..2643ce2d 100644
--- a/component_catalog/views.py
+++ b/component_catalog/views.py
@@ -251,7 +251,7 @@ class TabVulnerabilityMixin:
template = "component_catalog/tabs/tab_vulnerabilities.html"
def tab_vulnerabilities(self):
- vulnerabilities_qs = self.object.affected_by_vulnerabilities.all()
+ vulnerabilities_qs = self.object.affected_by_vulnerabilities.order_by_risk()
if not vulnerabilities_qs:
return
diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css
index 81ef895f..42d1c652 100644
--- a/dejacode/static/css/dejacode_bootstrap.css
+++ b/dejacode/static/css/dejacode_bootstrap.css
@@ -40,6 +40,9 @@ a.dropdown-item:hover {
.fs-85pct {
font-size: 85%;
}
+.fs-110pct {
+ font-size: 110%;
+}
.header {
margin-bottom: 1rem;
}
@@ -86,6 +89,9 @@ table.text-break thead {
word-wrap: initial!important;
word-break: initial!important;
}
+.bg-warning-orange {
+ background-color: var(--bs-orange);
+}
/* -- Dark there fixes -- */
[data-bs-theme=dark] .btn-outline-dark {
--bs-btn-color: var(--bs-tertiary-color);
@@ -380,14 +386,20 @@ table.vulnerabilities-table .column-summary {
#tab_vulnerabilities .column-vulnerability_id {
width: 210px;
}
-#tab_vulnerabilities .column-aliases {
- width: 210px;
+#tab_vulnerabilities .column-affected_packages {
+ min-width: 300px;
}
-#tab_vulnerabilities .column-max_score {
- width: 105px;
+#tab_vulnerabilities .column-exploitability {
+ width: 150px;
}
-#tab_vulnerabilities .column-column-affected_packages {
- width: 320px;
+#tab_vulnerabilities .column-weighted_severity {
+ width: 115px;
+}
+#tab_vulnerabilities .column-risk_score {
+ width: 95px;
+}
+#tab_vulnerabilities .column-summary {
+ width: 300px;
}
/* -- Dependency tab -- */
diff --git a/dje/copier.py b/dje/copier.py
index 385d510c..e3cfe4ab 100644
--- a/dje/copier.py
+++ b/dje/copier.py
@@ -66,6 +66,7 @@
"project_uuid",
"default_assignee",
"affected_by_vulnerabilities",
+ "risk_score",
]
diff --git a/dje/models.py b/dje/models.py
index 6d4fa452..e330520c 100644
--- a/dje/models.py
+++ b/dje/models.py
@@ -777,6 +777,11 @@ def update_from_data(self, user, data, override=False):
"""
Update this object instance with the provided `data`.
The `save()` method is called only if at least one field was modified.
+
+ The user is optional, providing None, as some context of automatic update are
+ not associated to a specific user.
+ We do not want to promote this as the default behavior thus we keep the user
+ a required parameter.
"""
model_fields = self.model_fields()
updated_fields = []
@@ -796,8 +801,11 @@ def update_from_data(self, user, data, override=False):
updated_fields.append(field_name)
if updated_fields:
- self.last_modified_by = user
- self.save(update_fields=[*updated_fields, "last_modified_by"])
+ if user:
+ self.last_modified_by = user
+ self.save(update_fields=[*updated_fields, "last_modified_by"])
+ else:
+ self.save(update_fields=updated_fields)
return updated_fields
diff --git a/dje/tasks.py b/dje/tasks.py
index 806cdd89..bcf0325e 100644
--- a/dje/tasks.py
+++ b/dje/tasks.py
@@ -312,4 +312,4 @@ def update_vulnerabilities():
for dataspace in dataspace_qs:
logger.info(f"fetch_vulnerabilities for datapsace={dataspace}")
- fetch_from_vulnerablecode(dataspace, batch_size=50, timeout=60)
+ fetch_from_vulnerablecode(dataspace, batch_size=50, update=True, timeout=60)
diff --git a/dje/tests/testfiles/test_dataset_cc_only.json b/dje/tests/testfiles/test_dataset_cc_only.json
index cb14fe0a..b80388ec 100644
--- a/dje/tests/testfiles/test_dataset_cc_only.json
+++ b/dje/tests/testfiles/test_dataset_cc_only.json
@@ -44,6 +44,7 @@
"last_modified_date": "2011-08-24T09:20:01Z",
"reference_notes": "",
"usage_policy": null,
+ "risk_score": null,
"declared_license_expression": "",
"other_license_expression": "",
"holder": "",
@@ -114,6 +115,7 @@
"last_modified_date": "2011-08-24T09:20:01Z",
"reference_notes": "",
"usage_policy": null,
+ "risk_score": null,
"declared_license_expression": "",
"other_license_expression": "",
"holder": "",
@@ -280,6 +282,7 @@
"version": "",
"qualifiers": "",
"subpath": "",
+ "risk_score": null,
"declared_license_expression": "",
"other_license_expression": "",
"holder": "",
diff --git a/dje/tests/testfiles/test_dataset_pp_only.json b/dje/tests/testfiles/test_dataset_pp_only.json
index fab48776..3c88ae44 100644
--- a/dje/tests/testfiles/test_dataset_pp_only.json
+++ b/dje/tests/testfiles/test_dataset_pp_only.json
@@ -16,6 +16,7 @@
"version": "",
"qualifiers": "",
"subpath": "",
+ "risk_score": null,
"declared_license_expression": "",
"other_license_expression": "",
"holder": "",
diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py
index e0fc5127..7a9785bc 100644
--- a/product_portfolio/filters.py
+++ b/product_portfolio/filters.py
@@ -31,6 +31,9 @@
from product_portfolio.models import ProductDependency
from product_portfolio.models import ProductPackage
from product_portfolio.models import ProductStatus
+from vulnerabilities.filters import RISK_SCORE_RANGES
+from vulnerabilities.filters import ScoreRangeFilter
+from vulnerabilities.models import Vulnerability
class ProductFilterSet(DataspacedFilterSet):
@@ -119,6 +122,7 @@ class Meta:
class BaseProductRelationFilterSet(DataspacedFilterSet):
+ field_name_prefix = None
is_deployed = BooleanChoiceFilter(
empty_label="All (Inventory)",
choices=(
@@ -130,9 +134,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
right_align=True,
),
)
-
is_modified = BooleanChoiceFilter()
-
object_type = django_filters.CharFilter(
method="filter_object_type",
widget=DropDownWidget(
@@ -145,6 +147,18 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
),
),
)
+ exploitability = django_filters.ChoiceFilter(
+ label=_("Exploitability"),
+ choices=Vulnerability.EXPLOITABILITY_CHOICES,
+ )
+ weighted_severity = ScoreRangeFilter(
+ label=_("Severity"),
+ score_ranges=RISK_SCORE_RANGES,
+ )
+ risk_score = ScoreRangeFilter(
+ label=_("Risk score"),
+ score_ranges=RISK_SCORE_RANGES,
+ )
@staticmethod
def filter_object_type(queryset, name, value):
@@ -176,8 +190,15 @@ def __init__(self, *args, **kwargs):
anchor=self.anchor, right_align=True
)
+ field_name_prefix = self.field_name_prefix
+ for field_name in ["exploitability", "weighted_severity", "risk_score"]:
+ field = self.filters[field_name]
+ field.extra["widget"] = DropDownWidget(anchor=self.anchor)
+ field.field_name = f"{field_name_prefix}__{field_name}"
+
class ProductComponentFilterSet(BaseProductRelationFilterSet):
+ field_name_prefix = "component"
q = SearchFilter(
label=_("Search"),
search_fields=[
@@ -226,6 +247,7 @@ class Meta:
class ProductPackageFilterSet(BaseProductRelationFilterSet):
+ field_name_prefix = "package"
q = SearchFilter(
label=_("Search"),
search_fields=[
@@ -265,10 +287,6 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
),
)
- @staticmethod
- def do_nothing(queryset, name, value):
- return queryset
-
class Meta:
model = ProductPackage
fields = [
diff --git a/product_portfolio/models.py b/product_portfolio/models.py
index da3eda4e..54c3582e 100644
--- a/product_portfolio/models.py
+++ b/product_portfolio/models.py
@@ -41,7 +41,7 @@
from dje.validators import generic_uri_validator
from dje.validators import validate_url_segment
from dje.validators import validate_version
-from vulnerabilities.fetch import fetch_for_queryset
+from vulnerabilities.fetch import fetch_for_packages
from vulnerabilities.models import Vulnerability
RELATION_LICENSE_EXPRESSION_HELP_TEXT = _(
@@ -510,7 +510,7 @@ def improve_packages_from_purldb(self, user):
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)
+ return fetch_for_packages(self.all_packages, self.dataspace)
def get_vulnerability_qs(self, prefetch_related_packages=False):
"""Return a QuerySet of all Vulnerability instances related to this product"""
@@ -542,7 +542,7 @@ class ProductItemPurpose(
label = models.CharField(
max_length=50,
help_text=_(
- "Concise name to identify the Purpose of the Product Component or " "Product Package."
+ "Concise name to identify the Purpose of the Product Component or Product Package."
),
)
diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html
index 4c37226b..400bc2f6 100644
--- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html
+++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html
@@ -40,30 +40,48 @@
{{ filter_productcomponent.form.object_type }}
{% if product.dataspace.enable_vulnerablecodedb_access %}
-
+
{{ filter_productcomponent.form.is_vulnerable }}
-
+
{% endif %}
| - {% trans 'Purpose' %} + + {% trans 'Purpose' %} + {{ filter_productcomponent.form.purpose }} | - {% trans 'Concluded license' %} + + {% trans 'Concluded license' %} + | -- {% trans 'Review status' %} + | + + {% trans 'Compliance status' %} + {{ filter_productcomponent.form.review_status }} | - {% trans 'Deployed' %} + + {% trans 'Deployed' %} + {{ filter_productcomponent.form.is_deployed }} | - {% trans 'Modified' %} + + {% trans 'Modified' %} + {{ filter_productcomponent.form.is_modified }} | + {% if product.dataspace.enable_vulnerablecodedb_access %} ++ + {% trans 'Risk' %} + + {{ filter_productcomponent.form.risk_score }} + | + {% endif %}{{ relation.review_status|default_if_none:'' }} | {{ relation.is_deployed|as_icon }} | {{ relation.is_modified|as_icon }} | + {% if product.dataspace.enable_vulnerablecodedb_access %} ++ {% if relation.related_component_or_package.vulnerability_count %} + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score only %} + {% endif %} + | + {% endif %} {% if relation.package and display_scan_features %}
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
- {% 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 %} + {% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %} + | ++ {{ vulnerability.weighted_severity|default_if_none:"" }} + | ++ {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} | {% if vulnerability.summary %} @@ -42,15 +51,6 @@ {% endif %} {% endif %} | -
-
|
||||||||||||||||
| - {% 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 %}
+
+ {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
+
|
{% if vulnerability.summary %} @@ -45,6 +35,15 @@ {% endif %} {% endif %} | ++ {% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %} + | ++ {{ vulnerability.weighted_severity|default_if_none:"" }} + | ++ {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + | {% if vulnerability.affected_products_count %} diff --git a/vulnerabilities/tests/data/vulnerabilities/idna_3.6_response.json b/vulnerabilities/tests/data/vulnerabilities/idna_3.6_response.json index 54c2b790..5b53f1fb 100644 --- a/vulnerabilities/tests/data/vulnerabilities/idna_3.6_response.json +++ b/vulnerabilities/tests/data/vulnerabilities/idna_3.6_response.json @@ -13,6 +13,7 @@ "qualifiers": {}, "subpath": "", "is_vulnerable": true, + "risk_score": 8.4, "next_non_vulnerable_version": "3.7", "latest_non_vulnerable_version": "3.7", "affected_by_vulnerabilities": [ @@ -20,6 +21,9 @@ "url": "http://public.vulnerablecode.io/api/vulnerabilities/525663", "vulnerability_id": "VCID-j3au-usaz-aaag", "summary": "Internationalized Domain Names in Applications (IDNA) vulnerable to denial of service from specially crafted inputs to idna.encode", + "exploitability": 2.0, + "weighted_severity": 4.2, + "risk_score": 8.4, "references": [ { "reference_url": "https://access.redhat.com/hydra/rest/securitydata/cve/CVE-2024-3651.json", diff --git a/vulnerabilities/tests/test_fetch.py b/vulnerabilities/tests/test_fetch.py index 77dfa5c3..47a105d8 100644 --- a/vulnerabilities/tests/test_fetch.py +++ b/vulnerabilities/tests/test_fetch.py @@ -8,6 +8,7 @@ import io import json +from decimal import Decimal from pathlib import Path from unittest import mock @@ -16,7 +17,7 @@ from component_catalog.models import Package from component_catalog.tests import make_package from dje.models import Dataspace -from vulnerabilities.fetch import fetch_for_queryset +from vulnerabilities.fetch import fetch_for_packages from vulnerabilities.fetch import fetch_from_vulnerablecode @@ -26,24 +27,26 @@ class VulnerabilitiesFetchTestCase(TestCase): def setUp(self): self.dataspace = Dataspace.objects.create(name="nexB") - @mock.patch("vulnerabilities.fetch.fetch_for_queryset") + @mock.patch("vulnerabilities.fetch.fetch_for_packages") @mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.is_configured") def test_vulnerabilities_fetch_from_vulnerablecode( - self, mock_is_configured, mock_fetch_for_queryset + self, mock_is_configured, mock_fetch_for_packages ): buffer = io.StringIO() make_package(self.dataspace, package_url="pkg:pypi/idna@3.6") make_package(self.dataspace, package_url="pkg:pypi/idna@2.0") mock_is_configured.return_value = True - mock_fetch_for_queryset.return_value = 2 - fetch_from_vulnerablecode(self.dataspace, batch_size=1, timeout=None, log_func=buffer.write) + mock_fetch_for_packages.return_value = 2 + fetch_from_vulnerablecode( + self.dataspace, batch_size=1, update=True, timeout=None, log_func=buffer.write + ) expected = "2 Packages in the queue.+ Created 2 vulnerabilitiesCompleted in 0 seconds" self.assertEqual(expected, buffer.getvalue()) self.dataspace.refresh_from_db() self.assertIsNotNone(self.dataspace.vulnerabilities_updated_at) @mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.bulk_search_by_purl") - def test_vulnerabilities_fetch_for_queryset(self, mock_bulk_search_by_purl): + def test_vulnerabilities_fetch_for_packages(self, mock_bulk_search_by_purl): buffer = io.StringIO() package1 = make_package(self.dataspace, package_url="pkg:pypi/idna@3.6") make_package(self.dataspace, package_url="pkg:pypi/idna@2.0") @@ -52,11 +55,27 @@ def test_vulnerabilities_fetch_for_queryset(self, mock_bulk_search_by_purl): response_json = json.loads(response_file.read_text()) mock_bulk_search_by_purl.return_value = response_json["results"] - created_vulnerabilities = fetch_for_queryset( - queryset, self.dataspace, batch_size=1, log_func=buffer.write + created_vulnerabilities = fetch_for_packages( + queryset, self.dataspace, batch_size=1, update=True, log_func=buffer.write ) self.assertEqual(1, created_vulnerabilities) self.assertEqual("Progress: 1/2Progress: 2/2", buffer.getvalue()) self.assertEqual(1, package1.affected_by_vulnerabilities.count()) vulnerability = package1.affected_by_vulnerabilities.get() self.assertEqual("VCID-j3au-usaz-aaag", vulnerability.vulnerability_id) + self.assertEqual(Decimal("2.0"), vulnerability.exploitability) + self.assertEqual(Decimal("4.2"), vulnerability.weighted_severity) + self.assertEqual(Decimal("8.4"), vulnerability.risk_score) + + package1.refresh_from_db() + self.assertEqual(Decimal("8.4"), package1.risk_score) + + # Update + response_json["results"][0]["affected_by_vulnerabilities"][0]["risk_score"] = 10.0 + mock_bulk_search_by_purl.return_value = response_json["results"] + created_vulnerabilities = fetch_for_packages( + queryset, self.dataspace, batch_size=1, update=True, log_func=buffer.write + ) + self.assertEqual(0, created_vulnerabilities) + vulnerability = package1.affected_by_vulnerabilities.get() + self.assertEqual(Decimal("10.0"), vulnerability.risk_score) diff --git a/vulnerabilities/tests/test_filters.py b/vulnerabilities/tests/test_filters.py index b10e5ad7..417ce5c2 100644 --- a/vulnerabilities/tests/test_filters.py +++ b/vulnerabilities/tests/test_filters.py @@ -16,12 +16,12 @@ class VulnerabilityFilterSetTestCase(TestCase): def setUp(self): self.dataspace = Dataspace.objects.create(name="Reference") - self.vulnerability1 = make_vulnerability(self.dataspace, max_score=10.0) + self.vulnerability1 = make_vulnerability(self.dataspace, risk_score=10.0) self.vulnerability2 = make_vulnerability( - self.dataspace, max_score=5.5, aliases=["ALIAS-V2"] + self.dataspace, risk_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) + self.vulnerability3 = make_vulnerability(self.dataspace, risk_score=2.0) + self.vulnerability4 = make_vulnerability(self.dataspace, risk_score=None) def test_vulnerability_filterset_search(self): data = {"q": self.vulnerability1.vulnerability_id} @@ -33,36 +33,36 @@ def test_vulnerability_filterset_search(self): self.assertQuerySetEqual(filterset.qs, [self.vulnerability2]) def test_vulnerability_filterset_sort_nulls_last_ordering(self): - data = {"sort": "max_score"} + data = {"sort": "risk_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.vulnerability4, # The risk_score=None are always last ] self.assertQuerySetEqual(filterset.qs, expected) - data = {"sort": "-max_score"} + data = {"sort": "-risk_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.vulnerability4, # The risk_score=None are always last ] self.assertQuerySetEqual(filterset.qs, expected) - def test_vulnerability_filterset_max_score(self): - data = {"max_score": "critical"} + def test_vulnerability_filterset_risk_score(self): + data = {"risk_score": "critical"} filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data) self.assertQuerySetEqual(filterset.qs, [self.vulnerability1]) - data = {"max_score": "high"} + data = {"risk_score": "high"} filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data) self.assertQuerySetEqual(filterset.qs, []) - data = {"max_score": "medium"} + data = {"risk_score": "medium"} filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data) self.assertQuerySetEqual(filterset.qs, [self.vulnerability2]) - data = {"max_score": "low"} + data = {"risk_score": "low"} filterset = VulnerabilityFilterSet(dataspace=self.dataspace, data=data) self.assertQuerySetEqual(filterset.qs, [self.vulnerability3]) diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index a504ab9a..05b00964 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -162,21 +162,8 @@ def test_vulnerability_model_create_from_data(self): self.assertEqual(vulnerability_data["aliases"], vulnerability1.aliases) self.assertEqual(vulnerability_data["references"], vulnerability1.references) self.assertEqual(vulnerability_data["resource_url"], vulnerability1.resource_url) - 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) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 7480031f..9b1b28c7 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -7,7 +7,6 @@ # from django.contrib.auth.mixins import LoginRequiredMixin -from django.db.models import F from django.http import Http404 from django.utils.translation import gettext_lazy as _ @@ -27,10 +26,10 @@ class VulnerabilityListView( template_list_table = "vulnerabilities/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("exploitability", _("Exploitability"), filter="exploitability"), + Header("weighted_severity", _("Severity"), filter="weighted_severity"), + Header("risk_score", _("Risk"), filter="risk_score"), 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"), @@ -47,18 +46,16 @@ def get_queryset(self): "aliases", "summary", "fixed_packages_count", - "max_score", - "min_score", + "exploitability", + "weighted_severity", + "risk_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", - ) + .order_by_risk() ) def get_context_data(self, **kwargs): |