From 9882b521284db5d498f62df9801e84bc3ca0bc1a Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 8 Nov 2024 17:24:36 +0400 Subject: [PATCH 01/31] Add a risk_score field and display the values in the UI #98 Signed-off-by: tdruez --- ...component_risk_score_package_risk_score.py | 23 +++++++++++ .../includes/risk_score_badge.html | 11 ++++++ .../tabs/tab_vulnerabilities.html | 25 +++++------- dejacode/static/css/dejacode_bootstrap.css | 3 ++ product_portfolio/filters.py | 39 ++++++++++++++++++- .../product_portfolio/tabs/tab_inventory.html | 17 +++++++- .../tabs/tab_vulnerabilities.html | 13 ++----- product_portfolio/views.py | 2 +- vulnerabilities/models.py | 10 +++++ 9 files changed, 113 insertions(+), 30 deletions(-) create mode 100644 component_catalog/migrations/0010_component_risk_score_package_risk_score.py create mode 100644 component_catalog/templates/component_catalog/includes/risk_score_badge.html 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..614567dd --- /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-08 13:24 + +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(decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), + ), + migrations.AddField( + model_name='package', + name='risk_score', + field=models.DecimalField(decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), + ), + ] diff --git a/component_catalog/templates/component_catalog/includes/risk_score_badge.html b/component_catalog/templates/component_catalog/includes/risk_score_badge.html new file mode 100644 index 00000000..865de9a7 --- /dev/null +++ b/component_catalog/templates/component_catalog/includes/risk_score_badge.html @@ -0,0 +1,11 @@ +{% if risk_score %} + + risk {{ risk_score }} + +{% endif %} \ No newline at end of file diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 77b010f8..f2714fbb 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 %} +
+
+ + Risk score + +
+
+
{{ package.risk_score }}
+
+
@@ -12,11 +22,6 @@ {% trans 'Aliases' %} - - + {% if product.dataspace.enable_vulnerablecodedb_access %} + + {% endif %} @@ -113,6 +119,13 @@ + {% if product.dataspace.enable_vulnerablecodedb_access %} + + {% endif %} {% if relation.package and display_scan_features %} diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index 1931d05d..b750797f 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -20,16 +20,6 @@ - + + + - {% empty %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 129229fa..3374c207 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -1096,10 +1096,11 @@ class ProductTabVulnerabilitiesView( filterset_class = VulnerabilityFilterSet table_headers = ( Header("vulnerability_id", _("Vulnerability")), - Header("aliases", _("Aliases")), - # Header("max_score", _("Score"), help_text="Severity score range", filter="max_score"), - Header("summary", _("Summary")), Header("affected_packages", _("Affected packages"), help_text="Affected product packages"), + Header("exploitability", _("Exploitability"), filter="max_score"), + Header("weighted_severity", _("Severity"), filter="max_score"), + Header("risk_score", _("Risk"), filter="max_score"), + Header("summary", _("Summary")), ) def get_context_data(self, **kwargs): diff --git a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py new file mode 100644 index 00000000..a434e37d --- /dev/null +++ b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py @@ -0,0 +1,32 @@ +# Generated by Django 5.0.9 on 2024-11-12 08:13 + +from django.db import migrations +import random +from decimal import Decimal + + +def set_random_risk_score(apps, schema_editor): + Vulnerability = apps.get_model("vulnerabilities", "Vulnerability") + + qs = Vulnerability.objects.all() + for vulnerability in qs: + exploitability = Decimal(f"{random.uniform(0.5, 2):.2f}") # 0.5 to 2 + weighted_severity = Decimal(f"{random.uniform(0, 10):.2f}") # 0 to 10 + risk_score = f"{min(float(exploitability) * float(weighted_severity), 10.0):.2f}" + + Vulnerability.objects.filter(pk=vulnerability.pk).update( + exploitability=exploitability, + weighted_severity=weighted_severity, + risk_score=risk_score, + ) + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0002_vulnerability_exploitability_and_more'), + ] + + operations = [ + migrations.RunPython(set_random_risk_score, reverse_code=migrations.RunPython.noop), + ] From c0af3df92d59727d9e0d5a104674aeca240f478b Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 12:55:06 +0400 Subject: [PATCH 08/31] Display new fields in Vulnerabilities lists #98 Signed-off-by: tdruez --- .../tabs/tab_vulnerabilities.html | 37 ++++++++++++++----- .../tables/vulnerability_list_table.html | 25 ++++++------- vulnerabilities/views.py | 6 +-- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index f2714fbb..f099dc07 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -17,16 +17,26 @@ {% trans 'Affected by' %} - - + + + + + + - - + + + - {% endif %} diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index ccd149d5..32ba9964 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -25,7 +25,7 @@ {% for package in vulnerability.affected_packages.all %}
  • {{ package }} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=package.risk_score only %} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=package.risk_score label='risk' only %}
  • {% endfor %} @@ -36,7 +36,7 @@ - - diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 4bdea090..42d1c652 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -390,13 +390,13 @@ table.vulnerabilities-table .column-summary { min-width: 300px; } #tab_vulnerabilities .column-exploitability { - width: 135px; + width: 150px; } #tab_vulnerabilities .column-weighted_severity { - width: 100px; + width: 115px; } #tab_vulnerabilities .column-risk_score { - width: 90px; + width: 95px; } #tab_vulnerabilities .column-summary { width: 300px; diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index b4610d17..ba5bf695 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 EXPLOITABILITY_RANGES +from vulnerabilities.filters import RISK_SCORE_RANGES +from vulnerabilities.filters import ScoreRangeFilter class ProductFilterSet(DataspacedFilterSet): @@ -118,19 +121,6 @@ class Meta: ] -risk_score_ranges = { - "low": (0.1, 2.9), - "medium": (3.0, 5.9), - "high": (6.0, 7.9), - "critical": (8.0, 10.0), -} - -RISK_SCORE_CHOICES = [ - (key, f"{key.capitalize()} ({value[0]} - {value[1]})") - for key, value in risk_score_ranges.items() -] - - class BaseProductRelationFilterSet(DataspacedFilterSet): is_deployed = BooleanChoiceFilter( empty_label="All (Inventory)", @@ -156,12 +146,20 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): ), ), ) - risk_score = django_filters.ChoiceFilter( + exploitability = ScoreRangeFilter( + label=_("Exploitability"), + field_name="package__exploitability", + score_ranges=EXPLOITABILITY_RANGES, + ) + weighted_severity = ScoreRangeFilter( + label=_("Severity"), + field_name="package__weighted_severity", + score_ranges=RISK_SCORE_RANGES, + ) + risk_score = ScoreRangeFilter( label=_("Risk score"), - choices=RISK_SCORE_CHOICES, - method="filter_by_risk_score_range", - help_text="Select a score range to filter.", - widget=DropDownWidget(anchor="#inventory", right_align=True), + field_name="package__risk_score", + score_ranges=RISK_SCORE_RANGES, ) @staticmethod @@ -181,20 +179,20 @@ def filter_object_type(queryset, name, value): return queryset.none() - def filter_by_risk_score_range(self, queryset, name, value): - if queryset.model is ProductPackage: - model_name = "package" - else: - model_name = "component" - - if value in risk_score_ranges: - low, high = risk_score_ranges[value] - filters = { - f"{model_name}__risk_score__gte": low, - f"{model_name}__risk_score__lte": high, - } - return queryset.filter(**filters) - return queryset + # def filter_by_risk_score_range(self, queryset, name, value): + # if queryset.model is ProductPackage: + # model_name = "package" + # else: + # model_name = "component" + # + # if value in risk_score_ranges: + # low, high = risk_score_ranges[value] + # filters = { + # f"{model_name}__risk_score__gte": low, + # f"{model_name}__risk_score__lte": high, + # } + # return queryset.filter(**filters) + # return queryset def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -209,6 +207,10 @@ def __init__(self, *args, **kwargs): anchor=self.anchor, right_align=True ) + self.filters["exploitability"].extra["widget"] = DropDownWidget(anchor=self.anchor) + self.filters["weighted_severity"].extra["widget"] = DropDownWidget(anchor=self.anchor) + self.filters["risk_score"].extra["widget"] = DropDownWidget(anchor=self.anchor) + class ProductComponentFilterSet(BaseProductRelationFilterSet): q = SearchFilter( diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 3374c207..b7badc1a 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -1097,9 +1097,9 @@ class ProductTabVulnerabilitiesView( table_headers = ( Header("vulnerability_id", _("Vulnerability")), Header("affected_packages", _("Affected packages"), help_text="Affected product packages"), - Header("exploitability", _("Exploitability"), filter="max_score"), - Header("weighted_severity", _("Severity"), filter="max_score"), - Header("risk_score", _("Risk"), filter="max_score"), + Header("exploitability", _("Exploitability"), filter="exploitability"), + Header("weighted_severity", _("Severity"), filter="weighted_severity"), + Header("risk_score", _("Risk"), filter="risk_score"), Header("summary", _("Summary")), ) diff --git a/vulnerabilities/filters.py b/vulnerabilities/filters.py index e1ae9e5a..3779fb99 100644 --- a/vulnerabilities/filters.py +++ b/vulnerabilities/filters.py @@ -17,6 +17,19 @@ from dje.widgets import SortDropDownWidget from vulnerabilities.models import Vulnerability +EXPLOITABILITY_RANGES = { + "no exploit known": (0.5, 0.9), + "exploit script published": (0.6, 1.5), + "high exploitability": (1.6, 2.0), +} + +RISK_SCORE_RANGES = { + "low": (0.1, 2.9), + "medium": (3.0, 5.9), + "high": (6.0, 7.9), + "critical": (8.0, 10.0), +} + class NullsLastOrderingFilter(django_filters.OrderingFilter): """ @@ -42,17 +55,26 @@ def filter(self, qs, value): return qs.order_by(*ordering) -vulnerability_score_ranges = { - "low": (0.1, 3), - "medium": (4.0, 6.9), - "high": (7.0, 8.9), - "critical": (9.0, 10.0), -} +class ScoreRangeFilter(django_filters.ChoiceFilter): + def __init__(self, *args, **kwargs): + score_ranges = kwargs.pop("score_ranges", {}) + choices = [ + (key, f"{key.capitalize()} ({value[0]} - {value[1]})") + for key, value in score_ranges.items() + ] + kwargs["choices"] = choices + super().__init__(*args, **kwargs) + self.score_ranges = score_ranges -SCORE_CHOICES = [ - (key, f"{key.capitalize()} ({value[0]} - {value[1]})") - for key, value in vulnerability_score_ranges.items() -] + def filter(self, qs, value): + if value in self.score_ranges: + low, high = self.score_ranges[value] + filters = { + f"{self.field_name}__gte": low, + f"{self.field_name}__lte": high, + } + return qs.filter(**filters) + return qs class VulnerabilityFilterSet(DataspacedFilterSet): @@ -63,8 +85,9 @@ class VulnerabilityFilterSet(DataspacedFilterSet): sort = NullsLastOrderingFilter( label=_("Sort"), fields=[ - "max_score", - "min_score", + "exploitability", + "weighted_severity", + "risk_score", "affected_products_count", "affected_packages_count", "fixed_packages_count", @@ -73,11 +96,17 @@ class VulnerabilityFilterSet(DataspacedFilterSet): ], widget=SortDropDownWidget, ) - max_score = django_filters.ChoiceFilter( - choices=SCORE_CHOICES, - method="filter_by_score_range", - label="Score Range", - help_text="Select a score range to filter.", + exploitability = ScoreRangeFilter( + label=_("Exploitability"), + score_ranges=EXPLOITABILITY_RANGES, + ) + weighted_severity = ScoreRangeFilter( + label=_("Severity"), + score_ranges=RISK_SCORE_RANGES, + ) + risk_score = ScoreRangeFilter( + label=_("Risk score"), + score_ranges=RISK_SCORE_RANGES, ) class Meta: @@ -88,10 +117,6 @@ class Meta: def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.filters["max_score"].extra["widget"] = DropDownRightWidget(anchor=self.anchor) - - def filter_by_score_range(self, queryset, name, value): - if value in vulnerability_score_ranges: - low, high = vulnerability_score_ranges[value] - return queryset.filter(max_score__gte=low, max_score__lte=high) - return queryset + self.filters["exploitability"].extra["widget"] = DropDownRightWidget(anchor=self.anchor) + self.filters["weighted_severity"].extra["widget"] = DropDownRightWidget(anchor=self.anchor) + self.filters["risk_score"].extra["widget"] = DropDownRightWidget(anchor=self.anchor) diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 39ea4166..d2959c0e 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -28,9 +28,9 @@ class VulnerabilityListView( table_headers = ( Header("vulnerability_id", _("Vulnerability")), Header("summary", _("Summary")), - Header("exploitability", _("Exploitability"), filter="max_score"), - Header("weighted_severity", _("Severity"), filter="max_score"), - Header("risk_score", _("Risk"), filter="max_score"), + 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"), From 4f37c7b3f513e60c10dd9a83016f1d08bba68210 Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 14:53:14 +0400 Subject: [PATCH 11/31] Sort the vulnerability by risk in listing #98 Signed-off-by: tdruez --- component_catalog/views.py | 2 +- product_portfolio/views.py | 6 +----- vulnerabilities/models.py | 7 +++++++ vulnerabilities/views.py | 11 ++++------- 4 files changed, 13 insertions(+), 13 deletions(-) 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/product_portfolio/views.py b/product_portfolio/views.py index b7badc1a..40dd71aa 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -25,7 +25,6 @@ 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 @@ -1111,10 +1110,7 @@ def get_context_data(self, **kwargs): 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", - ) + ).order_by_risk() self.filterset = self.filterset_class( self.request.GET, diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 7f843a8d..05e3d2d7 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -40,6 +40,13 @@ def with_affected_packages_count(self): affected_packages_count=Count("affected_packages", distinct=True), ) + def order_by_risk(self): + return self.order_by( + models.F("risk_score").desc(nulls_last=True), + models.F("weighted_severity").desc(nulls_last=True), + models.F("exploitability").desc(nulls_last=True), + ) + class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): """ diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index d2959c0e..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 _ @@ -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): From dba58069c82cf47f55a1788d390de0de791d9cc6 Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 14:57:21 +0400 Subject: [PATCH 12/31] Remove the min_score and max_score attributes from model #98 Signed-off-by: tdruez --- ...remove_vulnerability_max_score_and_more.py | 21 +++++++ vulnerabilities/models.py | 56 ------------------- 2 files changed, 21 insertions(+), 56 deletions(-) create mode 100644 vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py diff --git a/vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py b/vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py new file mode 100644 index 00000000..ef8d8db9 --- /dev/null +++ b/vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py @@ -0,0 +1,21 @@ +# Generated by Django 5.0.9 on 2024-11-12 10:54 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0003_to_delete_temp_fake_values'), + ] + + operations = [ + migrations.RemoveField( + model_name='vulnerability', + name='max_score', + ), + migrations.RemoveField( + model_name='vulnerability', + name='min_score', + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 05e3d2d7..ffe08960 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -101,16 +101,6 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): output_field=models.IntegerField(), db_persist=True, ) - min_score = models.FloatField( - null=True, - blank=True, - help_text=_("The minimum score of the range."), - ) - max_score = models.FloatField( - null=True, - blank=True, - help_text=_("The maximum score of the range."), - ) exploitability = models.DecimalField( null=True, max_digits=4, @@ -186,32 +176,8 @@ def add_affected_components(self, components): through_defaults = {"dataspace_id": self.dataspace_id} self.affected_components.add(*components, through_defaults=through_defaults) - @staticmethod - def range_to_values(self, range_str): - try: - min_score, max_score = range_str.split("-") - return float(min_score.strip()), float(max_score.strip()) - except Exception: - return - @classmethod def create_from_data(cls, dataspace, data, validate=False, affecting=None): - # Computing the min_score and max_score from the `references` as those data - # are not provided by the VulnerableCode API. - # https://github.com/aboutcode-org/vulnerablecode/issues/1573 - # severity_range_score = data.get("severity_range_score") - # if severity_range_score: - # min_score, max_score = self.range_to_values(severity_range_score) - # data["min_score"] = min_score - # data["max_score"] = max_score - - severities = [ - score for reference in data.get("references") for score in reference.get("scores", []) - ] - if scores := cls.get_severity_scores(severities): - data["min_score"] = min(scores) - data["max_score"] = max(scores) - instance = super().create_from_data(user=dataspace, data=data, validate=False) if affecting: @@ -219,28 +185,6 @@ def create_from_data(cls, dataspace, data, validate=False, affecting=None): return instance - @staticmethod - def get_severity_scores(severities): - score_map = { - "low": [0.1, 3], - "moderate": [4.0, 6.9], - "medium": [4.0, 6.9], - "high": [7.0, 8.9], - "important": [7.0, 8.9], - "critical": [9.0, 10.0], - } - - consolidated_scores = [] - for severity in severities: - score = severity.get("value") - try: - consolidated_scores.append(float(score)) - except ValueError: - if score_range := score_map.get(score.lower(), None): - consolidated_scores.extend(score_range) - - return consolidated_scores - def as_cyclonedx(self, affected_instances): affects = [ cdx_vulnerability.BomTarget(ref=instance.cyclonedx_bom_ref) From b727596c638cd489e0f7b758409cc5c0e9adc0da Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 17:58:16 +0400 Subject: [PATCH 13/31] Add help text in Inventory tab headers #98 Signed-off-by: tdruez --- product_portfolio/models.py | 2 +- .../product_portfolio/tabs/tab_inventory.html | 26 ++++++++++++++----- product_portfolio/views.py | 14 ++++++++++ 3 files changed, 34 insertions(+), 8 deletions(-) diff --git a/product_portfolio/models.py b/product_portfolio/models.py index fb9e5b60..54c3582e 100644 --- a/product_portfolio/models.py +++ b/product_portfolio/models.py @@ -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 0c972e38..e25672d4 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html @@ -42,31 +42,43 @@ {% if product.dataspace.enable_vulnerablecodedb_access %} {{ filter_productcomponent.form.is_vulnerable }} - + {% endif %} {% if product.dataspace.enable_vulnerablecodedb_access %} {% endif %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 40dd71aa..ee74b54d 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -59,6 +59,7 @@ from component_catalog.license_expression_dje import parse_expression from component_catalog.models import Component from component_catalog.models import Package +from component_catalog.models import Subcomponent from dejacode_toolkit.purldb import PurlDB from dejacode_toolkit.scancodeio import ScanCodeIO from dejacode_toolkit.scancodeio import get_hash_uid @@ -74,6 +75,7 @@ from dje.models import History from dje.templatetags.dje_tags import urlize_target_blank from dje.utils import chunked +from dje.utils import get_help_text from dje.utils import get_object_compare_diff from dje.utils import group_by_simple from dje.utils import is_uuid4 @@ -118,13 +120,16 @@ from product_portfolio.forms import ProductPackageInlineForm from product_portfolio.forms import PullProjectDataForm from product_portfolio.forms import TableInlineFormSetHelper +from product_portfolio.models import RELATION_LICENSE_EXPRESSION_HELP_TEXT from product_portfolio.models import CodebaseResource from product_portfolio.models import Product from product_portfolio.models import ProductComponent from product_portfolio.models import ProductDependency from product_portfolio.models import ProductPackage +from product_portfolio.models import ProductRelationshipMixin from product_portfolio.models import ScanCodeProject from vulnerabilities.filters import VulnerabilityFilterSet +from vulnerabilities.models import AffectedByVulnerabilityMixin from vulnerabilities.models import Vulnerability @@ -871,6 +876,15 @@ def get_context_data(self, **kwargs): } ) + context["help_texts"] = { + "purpose": get_help_text(Subcomponent, "purpose"), + "license_expression": RELATION_LICENSE_EXPRESSION_HELP_TEXT, + "review_status": get_help_text(ProductRelationshipMixin, "review_status"), + "is_deployed": get_help_text(ProductRelationshipMixin, "is_deployed"), + "is_modified": get_help_text(ProductRelationshipMixin, "is_modified"), + "risk_score": get_help_text(AffectedByVulnerabilityMixin, "risk_score"), + } + return context @staticmethod From 0bf3a1c955ef6c5b33089ce70a38fbe4dd8c7c64 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 14 Nov 2024 11:12:30 +0400 Subject: [PATCH 14/31] Remove dead code #98 Signed-off-by: tdruez --- product_portfolio/filters.py | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index ba5bf695..96a7a040 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -179,21 +179,6 @@ def filter_object_type(queryset, name, value): return queryset.none() - # def filter_by_risk_score_range(self, queryset, name, value): - # if queryset.model is ProductPackage: - # model_name = "package" - # else: - # model_name = "component" - # - # if value in risk_score_ranges: - # low, high = risk_score_ranges[value] - # filters = { - # f"{model_name}__risk_score__gte": low, - # f"{model_name}__risk_score__lte": high, - # } - # return queryset.filter(**filters) - # return queryset - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) From bef324e5f660213916b34a518413f7edbc41c08b Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 14 Nov 2024 15:00:34 +0400 Subject: [PATCH 15/31] Set proper choices for the exploitability filter #98 Signed-off-by: tdruez --- product_portfolio/filters.py | 6 +++--- .../product_portfolio/tabs/tab_inventory.html | 4 ++-- vulnerabilities/filters.py | 15 ++++++++------- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index 96a7a040..7a5035dc 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -31,7 +31,7 @@ from product_portfolio.models import ProductDependency from product_portfolio.models import ProductPackage from product_portfolio.models import ProductStatus -from vulnerabilities.filters import EXPLOITABILITY_RANGES +from vulnerabilities.filters import EXPLOITABILITY_CHOICES from vulnerabilities.filters import RISK_SCORE_RANGES from vulnerabilities.filters import ScoreRangeFilter @@ -146,10 +146,10 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): ), ), ) - exploitability = ScoreRangeFilter( + exploitability = django_filters.ChoiceFilter( label=_("Exploitability"), field_name="package__exploitability", - score_ranges=EXPLOITABILITY_RANGES, + choices=EXPLOITABILITY_CHOICES, ) weighted_severity = ScoreRangeFilter( label=_("Severity"), diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html index e25672d4..2318448c 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html @@ -132,9 +132,9 @@ {% if product.dataspace.enable_vulnerablecodedb_access %} - {% endif %} diff --git a/vulnerabilities/filters.py b/vulnerabilities/filters.py index 3779fb99..763e33d2 100644 --- a/vulnerabilities/filters.py +++ b/vulnerabilities/filters.py @@ -17,11 +17,12 @@ from dje.widgets import SortDropDownWidget from vulnerabilities.models import Vulnerability -EXPLOITABILITY_RANGES = { - "no exploit known": (0.5, 0.9), - "exploit script published": (0.6, 1.5), - "high exploitability": (1.6, 2.0), -} +EXPLOITABILITY_CHOICES = [ + (0.5, _("No exploit known (0.5)")), + (1.0, _("Exploit script published (1.0)")), + (2.0, _("High exploitability (2.0)")), +] + RISK_SCORE_RANGES = { "low": (0.1, 2.9), @@ -96,9 +97,9 @@ class VulnerabilityFilterSet(DataspacedFilterSet): ], widget=SortDropDownWidget, ) - exploitability = ScoreRangeFilter( + exploitability = django_filters.ChoiceFilter( label=_("Exploitability"), - score_ranges=EXPLOITABILITY_RANGES, + choices=EXPLOITABILITY_CHOICES, ) weighted_severity = ScoreRangeFilter( label=_("Severity"), From ed37abe698a77263bc02b3aca1f822fd295876a4 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 14 Nov 2024 15:00:49 +0400 Subject: [PATCH 16/31] Consolidate migrations #98 Signed-off-by: tdruez --- ...2_vulnerability_exploitability_and_more.py | 10 ++++++++- .../0003_to_delete_temp_fake_values.py | 2 +- ...remove_vulnerability_max_score_and_more.py | 21 ------------------- vulnerabilities/models.py | 2 +- 4 files changed, 11 insertions(+), 24 deletions(-) delete mode 100644 vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py diff --git a/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py b/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py index a244defe..cb769202 100644 --- a/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py +++ b/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py @@ -13,7 +13,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='vulnerability', name='exploitability', - field=models.DecimalField(decimal_places=2, help_text='Exploitability indicates the likelihood that a vulnerability in a software package could be used by malicious actors to compromise systems, applications, or networks. This metric is determined automatically based on the discovery of known exploits', max_digits=4, null=True), + field=models.DecimalField(decimal_places=2, help_text='Exploitability indicates the likelihood that a vulnerability in a software package could be used by malicious actors to compromise systems, applications, or networks. This metric is determined automatically based on the discovery of known exploits.', max_digits=4, null=True), ), migrations.AddField( model_name='vulnerability', @@ -25,4 +25,12 @@ class Migration(migrations.Migration): name='weighted_severity', field=models.DecimalField(decimal_places=2, help_text='Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.', max_digits=4, null=True), ), + migrations.RemoveField( + model_name='vulnerability', + name='max_score', + ), + migrations.RemoveField( + model_name='vulnerability', + name='min_score', + ), ] diff --git a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py index a434e37d..8ff2cdfc 100644 --- a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py +++ b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py @@ -10,7 +10,7 @@ def set_random_risk_score(apps, schema_editor): qs = Vulnerability.objects.all() for vulnerability in qs: - exploitability = Decimal(f"{random.uniform(0.5, 2):.2f}") # 0.5 to 2 + exploitability = random.choice([0.5, 1.0, 2.0]) # 0.5, 1.0, or 2.0 weighted_severity = Decimal(f"{random.uniform(0, 10):.2f}") # 0 to 10 risk_score = f"{min(float(exploitability) * float(weighted_severity), 10.0):.2f}" diff --git a/vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py b/vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py deleted file mode 100644 index ef8d8db9..00000000 --- a/vulnerabilities/migrations/0004_remove_vulnerability_max_score_and_more.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-12 10:54 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0003_to_delete_temp_fake_values'), - ] - - operations = [ - migrations.RemoveField( - model_name='vulnerability', - name='max_score', - ), - migrations.RemoveField( - model_name='vulnerability', - name='min_score', - ), - ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index ffe08960..648cf4c1 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -109,7 +109,7 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): "Exploitability indicates the likelihood that a vulnerability in a " "software package could be used by malicious actors to compromise systems, " "applications, or networks. This metric is determined automatically based " - "on the discovery of known exploits" + "on the discovery of known exploits." ), ) weighted_severity = models.DecimalField( From 750b65acdfb513a0a33362bde3d1591ddfd30c14 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 14 Nov 2024 16:08:25 +0400 Subject: [PATCH 17/31] Fix part of the failing tests #98 Signed-off-by: tdruez --- component_catalog/models.py | 1 + dje/copier.py | 1 + dje/tests/testfiles/test_dataset_cc_only.json | 3 +++ dje/tests/testfiles/test_dataset_pp_only.json | 1 + product_portfolio/tests/test_views.py | 8 ++++---- reporting/tests/test_models.py | 1 + vulnerabilities/models.py | 4 ++++ vulnerabilities/tests/test_models.py | 13 ------------- 8 files changed, 15 insertions(+), 17 deletions(-) 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/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/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/tests/test_views.py b/product_portfolio/tests/test_views.py index 505a84f2..a378b1e7 100644 --- a/product_portfolio/tests/test_views.py +++ b/product_portfolio/tests/test_views.py @@ -300,10 +300,10 @@ def test_product_portfolio_detail_view_tab_vulnerability_view_filters(self): self.client.login(username="nexb_user", password="secret") url = self.product1.get_url("tab_vulnerabilities") response = self.client.get(url) - self.assertContains(response, "?vulnerabilities-max_score=#vulnerabilities") - self.assertContains(response, "?vulnerabilities-sort=max_score#vulnerabilities") - response = self.client.get(url + "?vulnerabilities-sort=max_score#vulnerabilities") - self.assertContains(response, "?vulnerabilities-sort=-max_score#vulnerabilities") + self.assertContains(response, "?vulnerabilities-risk_score=#vulnerabilities") + self.assertContains(response, "?vulnerabilities-sort=risk_score#vulnerabilities") + response = self.client.get(url + "?vulnerabilities-sort=risk_score#vulnerabilities") + self.assertContains(response, "?vulnerabilities-sort=-risk_score#vulnerabilities") @mock.patch("dejacode_toolkit.vulnerablecode.VulnerableCode.is_configured") def test_product_portfolio_detail_view_tab_vulnerability_label(self, mock_is_configured): diff --git a/reporting/tests/test_models.py b/reporting/tests/test_models.py index efbe0604..6a973d65 100644 --- a/reporting/tests/test_models.py +++ b/reporting/tests/test_models.py @@ -2016,6 +2016,7 @@ def test_get_model_data_for_component_column_template(self): {"group": "Direct Fields", "value": "reference_notes", "label": "reference_notes"}, {"group": "Direct Fields", "label": "release_date", "value": "release_date"}, {"group": "Direct Fields", "label": "request_count", "value": "request_count"}, + {"group": "Direct Fields", "label": "risk_score", "value": "risk_score"}, { "group": "Direct Fields", "label": "sublicense_allowed", diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 648cf4c1..af17b1ad 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -103,6 +103,7 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): ) exploitability = models.DecimalField( null=True, + blank=True, max_digits=4, decimal_places=2, help_text=_( @@ -114,6 +115,7 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): ) weighted_severity = models.DecimalField( null=True, + blank=True, max_digits=4, decimal_places=2, help_text=_( @@ -123,6 +125,7 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): ) risk_score = models.DecimalField( null=True, + blank=True, max_digits=4, decimal_places=2, help_text=_( @@ -363,6 +366,7 @@ class AffectedByVulnerabilityMixin(models.Model): # Based on vulnerablecode.vulnerabilities.models.Package risk_score = models.DecimalField( null=True, + blank=True, max_digits=4, decimal_places=2, help_text=_( 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) From 5f82ebb44ef882f388b925514bb4eab52f44ff97 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 14 Nov 2024 16:09:16 +0400 Subject: [PATCH 18/31] Add migration files #98 Signed-off-by: tdruez --- ...ent_risk_score_alter_package_risk_score.py | 23 +++++++++++++++ ...r_vulnerability_exploitability_and_more.py | 28 +++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py create mode 100644 vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py diff --git a/component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py b/component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py new file mode 100644 index 00000000..f11c7c43 --- /dev/null +++ b/component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.9 on 2024-11-14 12:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0011_to_delete_temp_fake_values'), + ] + + operations = [ + migrations.AlterField( + model_name='component', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), + ), + migrations.AlterField( + model_name='package', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), + ), + ] diff --git a/vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py b/vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py new file mode 100644 index 00000000..87070fe0 --- /dev/null +++ b/vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.9 on 2024-11-14 12:08 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0003_to_delete_temp_fake_values'), + ] + + operations = [ + migrations.AlterField( + model_name='vulnerability', + name='exploitability', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Exploitability indicates the likelihood that a vulnerability in a software package could be used by malicious actors to compromise systems, applications, or networks. This metric is determined automatically based on the discovery of known exploits.', max_digits=4, null=True), + ), + migrations.AlterField( + model_name='vulnerability', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Risk score from 0.00 to 10.00, with higher values indicating greater vulnerability risk. This score is the maximum of the weighted severity multiplied by exploitability, capped at 10.', max_digits=4, null=True), + ), + migrations.AlterField( + model_name='vulnerability', + name='weighted_severity', + field=models.DecimalField(blank=True, decimal_places=2, help_text='Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.', max_digits=4, null=True), + ), + ] From 8aa4ceca78f6b583116c46bba5df13e72219f595 Mon Sep 17 00:00:00 2001 From: tdruez Date: Thu, 14 Nov 2024 16:20:11 +0400 Subject: [PATCH 19/31] Fix failing tests #98 Signed-off-by: tdruez --- vulnerabilities/tests/test_filters.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) 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]) From 1bd041a26cc9cbd086015ac94151c116e36efb20 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 15 Nov 2024 10:30:50 +0400 Subject: [PATCH 20/31] Refine the DecimalField and exploitability label system #98 Signed-off-by: tdruez --- ...ent_risk_score_alter_package_risk_score.py | 23 ++++++++++++ .../tabs/tab_vulnerabilities.html | 2 +- product_portfolio/filters.py | 4 +-- .../tabs/tab_vulnerabilities.html | 2 +- vulnerabilities/filters.py | 12 +------ ...remove_vulnerability_max_score_and_more.py | 36 +++++++++++++++++++ ...2_vulnerability_exploitability_and_more.py | 36 ------------------- .../0003_to_delete_temp_fake_values.py | 2 +- ...r_vulnerability_exploitability_and_more.py | 28 --------------- vulnerabilities/models.py | 32 ++++++++++------- .../tables/vulnerability_list_table.html | 2 +- 11 files changed, 85 insertions(+), 94 deletions(-) create mode 100644 component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py create mode 100644 vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py delete mode 100644 vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py delete mode 100644 vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py diff --git a/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py b/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py new file mode 100644 index 00000000..bca35e6e --- /dev/null +++ b/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py @@ -0,0 +1,23 @@ +# Generated by Django 5.0.9 on 2024-11-15 06:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0012_alter_component_risk_score_alter_package_risk_score'), + ] + + operations = [ + migrations.AlterField( + model_name='component', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True), + ), + migrations.AlterField( + model_name='package', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True), + ), + ] diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 37a54a8a..379957a2 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -75,7 +75,7 @@ {% endif %} {% endif %} diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index d7860ced..39633391 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -25,19 +25,19 @@ {% for package in vulnerability.affected_packages.all %}
  • {{ package }} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=package.risk_score label='risk' only %} + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=package.risk_score label='risk' only %}
  • {% endfor %}
    - - {% trans 'Score' %} - - {% trans 'Summary' %} @@ -47,16 +52,6 @@ {% 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 %} diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 81ef895f..5b6d9c51 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -86,6 +86,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); diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index e0fc5127..f2f27eba 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -118,6 +118,19 @@ class Meta: ] +risk_score_ranges = { + "low": (0.1, 2.9), + "medium": (3.0, 5.9), + "high": (6.0, 7.9), + "critical": (8.0, 10.0), +} + +RISK_SCORE_CHOICES = [ + (key, f"{key.capitalize()} ({value[0]} - {value[1]})") + for key, value in risk_score_ranges.items() +] + + class BaseProductRelationFilterSet(DataspacedFilterSet): is_deployed = BooleanChoiceFilter( empty_label="All (Inventory)", @@ -130,9 +143,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): right_align=True, ), ) - is_modified = BooleanChoiceFilter() - object_type = django_filters.CharFilter( method="filter_object_type", widget=DropDownWidget( @@ -145,6 +156,15 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): ), ), ) + risk_score = django_filters.ChoiceFilter( + label=_("Risk score"), + choices=RISK_SCORE_CHOICES, + method="filter_by_risk_score_range", + help_text="Select a score range to filter.", + widget=DropDownWidget( + anchor="#inventory", right_align=True + ), + ) @staticmethod def filter_object_type(queryset, name, value): @@ -163,6 +183,21 @@ def filter_object_type(queryset, name, value): return queryset.none() + def filter_by_risk_score_range(self, queryset, name, value): + if queryset.model is ProductPackage: + model_name = "package" + else: + model_name = "component" + + if value in risk_score_ranges: + low, high = risk_score_ranges[value] + filters = { + f"{model_name}__risk_score__gte": low, + f"{model_name}__risk_score__lte": high, + } + return queryset.filter(**filters) + return queryset + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html index 39974053..1af1c3f6 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html @@ -40,7 +40,7 @@ {{ filter_productcomponent.form.object_type }} {% if product.dataspace.enable_vulnerablecodedb_access %} -
    + {{ filter_productcomponent.form.is_vulnerable }}
    {% endif %} @@ -53,7 +53,7 @@ {% trans 'Concluded license' %}
    - {% trans 'Review status' %} + {% trans 'Compliance status' %} {{ filter_productcomponent.form.review_status }} @@ -64,6 +64,12 @@ {% trans 'Modified' %} {{ filter_productcomponent.form.is_modified }} + {% trans 'Risk' %} + {{ filter_productcomponent.form.risk_score }} +
    {{ relation.review_status|default_if_none:'' }} {{ relation.is_deployed|as_icon }} {{ relation.is_modified|as_icon }} + {% if relation.related_component_or_package.vulnerability_count %} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score only %} + {% 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 %} - {% if vulnerability.summary %} {% if vulnerability.summary|length > 120 %} @@ -47,6 +37,9 @@ {% for package in vulnerability.affected_packages.all %}
  • {{ package }} + {% if package.risk_score %} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=package.risk_score only %} + {% endif %}
  • {% endfor %} diff --git a/product_portfolio/views.py b/product_portfolio/views.py index 0a0a20fc..129229fa 100644 --- a/product_portfolio/views.py +++ b/product_portfolio/views.py @@ -1097,7 +1097,7 @@ class ProductTabVulnerabilitiesView( table_headers = ( Header("vulnerability_id", _("Vulnerability")), Header("aliases", _("Aliases")), - Header("max_score", _("Score"), help_text="Severity score range", filter="max_score"), + # Header("max_score", _("Score"), help_text="Severity score range", filter="max_score"), Header("summary", _("Summary")), Header("affected_packages", _("Affected packages"), help_text="Affected product packages"), ) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index dbd789b4..e742ac48 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -378,6 +378,16 @@ class AffectedByVulnerabilityMixin(models.Model): related_name="affected_%(class)ss", help_text=_("Vulnerabilities affecting this object."), ) + # Based on vulnerablecode.vulnerabilities.models.Package + risk_score = models.DecimalField( + null=True, + max_digits=4, + decimal_places=2, + help_text=_( + "Risk score between 0.00 and 10.00, where higher values " + "indicate greater vulnerability risk for the package." + ), + ) class Meta: abstract = True From 97e1bb95522d4f8bfff0174ca9ee61404609ca4b Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 8 Nov 2024 17:35:28 +0400 Subject: [PATCH 02/31] Generate random value for the risk_score #98 Signed-off-by: tdruez --- .../0011_to_delete_temp_fake_values.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 component_catalog/migrations/0011_to_delete_temp_fake_values.py diff --git a/component_catalog/migrations/0011_to_delete_temp_fake_values.py b/component_catalog/migrations/0011_to_delete_temp_fake_values.py new file mode 100644 index 00000000..5840dacf --- /dev/null +++ b/component_catalog/migrations/0011_to_delete_temp_fake_values.py @@ -0,0 +1,35 @@ +# Generated by Django 5.0.9 on 2024-11-08 13:26 + +from django.db import migrations +from django.db.models import Count + + +def generate_random_risk_score(): + import random + from decimal import Decimal + + return Decimal(f"{random.uniform(0, 10):.2f}") + + +def set_random_risk_score(apps, schema_editor): + Package = apps.get_model("component_catalog", "Package") + + qs = Package.objects.annotate( + vulnerability_count=Count("affected_by_vulnerabilities", distinct=True) + ).filter(vulnerability_count__gt=0) + + for package in qs: + risk_score = generate_random_risk_score() + Package.objects.filter(pk=package.pk).update(risk_score=risk_score) + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0010_component_risk_score_package_risk_score'), + ] + + operations = [ + migrations.RunPython(set_random_risk_score), + ] + From bbad3da9e000f9f42364e1a6745f2c5142e8f534 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 11 Nov 2024 15:56:47 +0400 Subject: [PATCH 03/31] Remove temp data migration #98 Signed-off-by: tdruez --- .../0011_to_delete_temp_fake_values.py | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 component_catalog/migrations/0011_to_delete_temp_fake_values.py diff --git a/component_catalog/migrations/0011_to_delete_temp_fake_values.py b/component_catalog/migrations/0011_to_delete_temp_fake_values.py deleted file mode 100644 index 5840dacf..00000000 --- a/component_catalog/migrations/0011_to_delete_temp_fake_values.py +++ /dev/null @@ -1,35 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-08 13:26 - -from django.db import migrations -from django.db.models import Count - - -def generate_random_risk_score(): - import random - from decimal import Decimal - - return Decimal(f"{random.uniform(0, 10):.2f}") - - -def set_random_risk_score(apps, schema_editor): - Package = apps.get_model("component_catalog", "Package") - - qs = Package.objects.annotate( - vulnerability_count=Count("affected_by_vulnerabilities", distinct=True) - ).filter(vulnerability_count__gt=0) - - for package in qs: - risk_score = generate_random_risk_score() - Package.objects.filter(pk=package.pk).update(risk_score=risk_score) - - -class Migration(migrations.Migration): - - dependencies = [ - ('component_catalog', '0010_component_risk_score_package_risk_score'), - ] - - operations = [ - migrations.RunPython(set_random_risk_score), - ] - From b90cf9dcfadd921f79ab162542f3ec3597578fa0 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 11 Nov 2024 16:12:02 +0400 Subject: [PATCH 04/31] CSS adjustments #98 Signed-off-by: tdruez --- dejacode/static/css/dejacode_bootstrap.css | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 5b6d9c51..be285f69 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -389,8 +389,8 @@ table.vulnerabilities-table .column-summary { #tab_vulnerabilities .column-max_score { width: 105px; } -#tab_vulnerabilities .column-column-affected_packages { - width: 320px; +#tab_vulnerabilities .column-affected_packages { + min-width: 350px; } /* -- Dependency tab -- */ From 57d5acd4616ddd0e9d57de68affbdc6773deddc7 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 11 Nov 2024 16:23:51 +0400 Subject: [PATCH 05/31] Rename fetch_for_queryset to fetch_for_packages #98 Signed-off-by: tdruez --- component_catalog/importers.py | 4 ++-- component_catalog/tests/test_importers.py | 8 ++++---- product_portfolio/filters.py | 4 +--- product_portfolio/models.py | 4 ++-- vulnerabilities/fetch.py | 4 ++-- vulnerabilities/tests/test_fetch.py | 12 ++++++------ 6 files changed, 17 insertions(+), 19 deletions(-) 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/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/product_portfolio/filters.py b/product_portfolio/filters.py index f2f27eba..b4610d17 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -161,9 +161,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): choices=RISK_SCORE_CHOICES, method="filter_by_risk_score_range", help_text="Select a score range to filter.", - widget=DropDownWidget( - anchor="#inventory", right_align=True - ), + widget=DropDownWidget(anchor="#inventory", right_align=True), ) @staticmethod diff --git a/product_portfolio/models.py b/product_portfolio/models.py index da3eda4e..fb9e5b60 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""" diff --git a/vulnerabilities/fetch.py b/vulnerabilities/fetch.py index e5fe9bc9..e17a244c 100644 --- a/vulnerabilities/fetch.py +++ b/vulnerabilities/fetch.py @@ -38,7 +38,7 @@ def fetch_from_vulnerablecode(dataspace, batch_size, timeout, log_func=None): if log_func: log_func(f"{package_count} Packages in the queue.") - created = fetch_for_queryset(package_qs, dataspace, batch_size, timeout, log_func) + created = fetch_for_packages(package_qs, dataspace, batch_size, timeout, log_func) run_time = timer() - start_time if log_func: log_func(f"+ Created {intcomma(created)} vulnerabilities") @@ -48,7 +48,7 @@ def fetch_from_vulnerablecode(dataspace, batch_size, timeout, log_func=None): dataspace.save(update_fields=["vulnerabilities_updated_at"]) -def fetch_for_queryset(queryset, dataspace, batch_size=50, timeout=None, log_func=None): +def fetch_for_packages(queryset, dataspace, batch_size=50, timeout=None, log_func=None): object_count = queryset.count() if object_count < 1: return diff --git a/vulnerabilities/tests/test_fetch.py b/vulnerabilities/tests/test_fetch.py index 77dfa5c3..1f79055e 100644 --- a/vulnerabilities/tests/test_fetch.py +++ b/vulnerabilities/tests/test_fetch.py @@ -16,7 +16,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,16 +26,16 @@ 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 + mock_fetch_for_packages.return_value = 2 fetch_from_vulnerablecode(self.dataspace, batch_size=1, timeout=None, log_func=buffer.write) expected = "2 Packages in the queue.+ Created 2 vulnerabilitiesCompleted in 0 seconds" self.assertEqual(expected, buffer.getvalue()) @@ -43,7 +43,7 @@ def test_vulnerabilities_fetch_from_vulnerablecode( 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,7 +52,7 @@ 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( + created_vulnerabilities = fetch_for_packages( queryset, self.dataspace, batch_size=1, log_func=buffer.write ) self.assertEqual(1, created_vulnerabilities) From 276ac5b4e35ebf438e73722b04b899bbb90e26dd Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 12:02:34 +0400 Subject: [PATCH 06/31] Add exploitability, weighted_severity, risk_score on Vulnerability #98 Signed-off-by: tdruez --- ...2_vulnerability_exploitability_and_more.py | 28 +++++++++++++++++ vulnerabilities/models.py | 31 +++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py diff --git a/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py b/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py new file mode 100644 index 00000000..a244defe --- /dev/null +++ b/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py @@ -0,0 +1,28 @@ +# Generated by Django 5.0.9 on 2024-11-12 08:02 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='vulnerability', + name='exploitability', + field=models.DecimalField(decimal_places=2, help_text='Exploitability indicates the likelihood that a vulnerability in a software package could be used by malicious actors to compromise systems, applications, or networks. This metric is determined automatically based on the discovery of known exploits', max_digits=4, null=True), + ), + migrations.AddField( + model_name='vulnerability', + name='risk_score', + field=models.DecimalField(decimal_places=2, help_text='Risk score from 0.00 to 10.00, with higher values indicating greater vulnerability risk. This score is the maximum of the weighted severity multiplied by exploitability, capped at 10.', max_digits=4, null=True), + ), + migrations.AddField( + model_name='vulnerability', + name='weighted_severity', + field=models.DecimalField(decimal_places=2, help_text='Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.', max_digits=4, null=True), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index e742ac48..7f843a8d 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -104,6 +104,37 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): blank=True, help_text=_("The maximum score of the range."), ) + exploitability = models.DecimalField( + null=True, + max_digits=4, + decimal_places=2, + help_text=_( + "Exploitability indicates the likelihood that a vulnerability in a " + "software package could be used by malicious actors to compromise systems, " + "applications, or networks. This metric is determined automatically based " + "on the discovery of known exploits" + ), + ) + weighted_severity = models.DecimalField( + null=True, + max_digits=4, + decimal_places=2, + help_text=_( + "Weighted severity is the highest value calculated by multiplying each " + "severity by its corresponding weight, divided by 10." + ), + ) + risk_score = models.DecimalField( + null=True, + max_digits=4, + decimal_places=2, + help_text=_( + "Risk score from 0.00 to 10.00, with higher values indicating greater " + "vulnerability risk. " + "This score is the maximum of the weighted severity multiplied by " + "exploitability, capped at 10." + ), + ) objects = DataspacedManager.from_queryset(VulnerabilityQuerySet)() From b72c6c968a21c353353890bdc2afe6ef4eaae90c Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 12:32:41 +0400 Subject: [PATCH 07/31] Display new fields in Vulnerabilities tab #98 Signed-off-by: tdruez --- .../0011_to_delete_temp_fake_values.py | 34 +++++++++++++++++++ dejacode/static/css/dejacode_bootstrap.css | 18 ++++++---- .../tabs/tab_vulnerabilities.html | 33 +++++++++++------- product_portfolio/views.py | 7 ++-- .../0003_to_delete_temp_fake_values.py | 32 +++++++++++++++++ 5 files changed, 102 insertions(+), 22 deletions(-) create mode 100644 component_catalog/migrations/0011_to_delete_temp_fake_values.py create mode 100644 vulnerabilities/migrations/0003_to_delete_temp_fake_values.py diff --git a/component_catalog/migrations/0011_to_delete_temp_fake_values.py b/component_catalog/migrations/0011_to_delete_temp_fake_values.py new file mode 100644 index 00000000..4999d7ef --- /dev/null +++ b/component_catalog/migrations/0011_to_delete_temp_fake_values.py @@ -0,0 +1,34 @@ +# Generated by Django 5.0.9 on 2024-11-12 08:13 + +from django.db import migrations +from django.db.models import Count + + +def generate_random_risk_score(): + import random + from decimal import Decimal + + return Decimal(f"{random.uniform(0, 10):.2f}") + + +def set_random_risk_score(apps, schema_editor): + Package = apps.get_model("component_catalog", "Package") + + qs = Package.objects.annotate( + vulnerability_count=Count("affected_by_vulnerabilities", distinct=True) + ).filter(vulnerability_count__gt=0) + + for package in qs: + risk_score = generate_random_risk_score() + Package.objects.filter(pk=package.pk).update(risk_score=risk_score) + + +class Migration(migrations.Migration): + + dependencies = [ + ('component_catalog', '0010_component_risk_score_package_risk_score'), + ] + + operations = [ + migrations.RunPython(set_random_risk_score, reverse_code=migrations.RunPython.noop), + ] diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index be285f69..8e67a9cf 100644 --- a/dejacode/static/css/dejacode_bootstrap.css +++ b/dejacode/static/css/dejacode_bootstrap.css @@ -383,14 +383,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: 135px; } -#tab_vulnerabilities .column-affected_packages { - min-width: 350px; +#tab_vulnerabilities .column-weighted_severity { + width: 100px; +} +#tab_vulnerabilities .column-risk_score { + width: 90px; +} +#tab_vulnerabilities .column-summary { + width: 300px; } /* -- Dependency tab -- */ diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index b750797f..ccd149d5 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -16,9 +16,28 @@ {{ vulnerability.vulnerability_id }} {% endif %} +
    + {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %} +
    - {% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %} +
      + {% for package in vulnerability.affected_packages.all %} +
    • + {{ package }} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=package.risk_score only %} +
    • + {% endfor %} +
    +
    + {{ vulnerability.exploitability|default_if_none:"" }} + + {{ vulnerability.weighted_severity|default_if_none:"" }} + + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} {% if vulnerability.summary %} @@ -32,18 +51,6 @@ {% endif %} {% endif %} -
      - {% for package in vulnerability.affected_packages.all %} -
    • - {{ package }} - {% if package.risk_score %} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=package.risk_score only %} - {% endif %} -
    • - {% endfor %} -
    -
    - - {% trans 'Aliases' %} - - + {% trans 'Summary' %} + + {% trans 'Exploitability' %} + + + + {% trans 'Severity' %} + + + + {% trans 'Risk' %} + + {% trans 'Fixed packages' %} @@ -48,9 +58,9 @@ {{ vulnerability.vulnerability_id }} {% endif %} - - - {% 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.summary %} @@ -64,6 +74,15 @@ {% endif %} {% endif %} + {{ vulnerability.exploitability|default_if_none:"" }} + + {{ vulnerability.weighted_severity|default_if_none:"" }} + + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + {% if vulnerability.fixed_packages_html %} {{ vulnerability.fixed_packages_html }} diff --git a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html index 012e9c6b..4ebe536c 100644 --- a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html +++ b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html @@ -19,19 +19,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 %} @@ -45,6 +35,15 @@ {% endif %} {% endif %} + {{ vulnerability.exploitability|default_if_none:"" }} + + {{ vulnerability.weighted_severity|default_if_none:"" }} + + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + {% if vulnerability.affected_products_count %} diff --git a/vulnerabilities/views.py b/vulnerabilities/views.py index 7480031f..39ea4166 100644 --- a/vulnerabilities/views.py +++ b/vulnerabilities/views.py @@ -27,10 +27,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="max_score"), + Header("weighted_severity", _("Severity"), filter="max_score"), + Header("risk_score", _("Risk"), filter="max_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"), From f281f13f2ed3155955b5f125a1a7153475964c82 Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 13:52:48 +0400 Subject: [PATCH 09/31] Refine the Risk badge rendering #98 Signed-off-by: tdruez --- .../component_catalog/includes/risk_score_badge.html | 5 ++++- .../component_catalog/tabs/tab_vulnerabilities.html | 2 +- dejacode/static/css/dejacode_bootstrap.css | 3 +++ .../templates/product_portfolio/tabs/tab_inventory.html | 2 +- .../product_portfolio/tabs/tab_vulnerabilities.html | 4 ++-- .../vulnerabilities/tables/vulnerability_list_table.html | 2 +- 6 files changed, 12 insertions(+), 6 deletions(-) diff --git a/component_catalog/templates/component_catalog/includes/risk_score_badge.html b/component_catalog/templates/component_catalog/includes/risk_score_badge.html index 865de9a7..832315a9 100644 --- a/component_catalog/templates/component_catalog/includes/risk_score_badge.html +++ b/component_catalog/templates/component_catalog/includes/risk_score_badge.html @@ -6,6 +6,9 @@ {% else %}bg-secondary {% endif %} "> - risk {{ risk_score }} + {% if label %} + {{ label }} + {% endif %} + {{ risk_score }} {% endif %} \ No newline at end of file diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index f099dc07..090de29c 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -80,7 +80,7 @@ {{ vulnerability.weighted_severity|default_if_none:"" }} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} diff --git a/dejacode/static/css/dejacode_bootstrap.css b/dejacode/static/css/dejacode_bootstrap.css index 8e67a9cf..4bdea090 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; } diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html index 1af1c3f6..0c972e38 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html @@ -122,7 +122,7 @@ {% if product.dataspace.enable_vulnerablecodedb_access %} {% if relation.related_component_or_package.vulnerability_count %} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score only %} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score label='risk' only %} {% endif %} {{ vulnerability.weighted_severity|default_if_none:"" }} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} diff --git a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html index 4ebe536c..4163a641 100644 --- a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html +++ b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html @@ -41,7 +41,7 @@ {{ vulnerability.weighted_severity|default_if_none:"" }} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} From 7d5b4a6473001b75f07fd8139c7504d5ecf01d20 Mon Sep 17 00:00:00 2001 From: tdruez Date: Tue, 12 Nov 2024 14:45:50 +0400 Subject: [PATCH 10/31] Add filter for all new fields #98 Signed-off-by: tdruez --- .../tabs/tab_vulnerabilities.html | 6 +- dejacode/static/css/dejacode_bootstrap.css | 6 +- product_portfolio/filters.py | 66 +++++++++-------- product_portfolio/views.py | 6 +- vulnerabilities/filters.py | 73 +++++++++++++------ vulnerabilities/views.py | 6 +- 6 files changed, 95 insertions(+), 68 deletions(-) diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 090de29c..37a54a8a 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -23,17 +23,17 @@ - + {% trans 'Exploitability' %} - + {% trans 'Severity' %} - + {% trans 'Risk' %} - {% trans 'Purpose' %} + + {% trans 'Purpose' %} + {{ filter_productcomponent.form.purpose }} - {% trans 'Concluded license' %} + + {% trans 'Concluded license' %} + - {% trans 'Compliance 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 }} - {% trans 'Risk' %} + + {% trans 'Risk' %} + {{ filter_productcomponent.form.risk_score }} {{ relation.is_deployed|as_icon }} {{ relation.is_modified|as_icon }} + {% if relation.related_component_or_package.vulnerability_count %} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score label='risk' only %} + {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score only %} {% endif %} - {{ vulnerability.exploitability|default_if_none:"" }} + {{ vulnerability.get_exploitability_display }} {{ vulnerability.weighted_severity|default_if_none:"" }} diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index 7a5035dc..9764c6b7 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -31,9 +31,9 @@ from product_portfolio.models import ProductDependency from product_portfolio.models import ProductPackage from product_portfolio.models import ProductStatus -from vulnerabilities.filters import EXPLOITABILITY_CHOICES from vulnerabilities.filters import RISK_SCORE_RANGES from vulnerabilities.filters import ScoreRangeFilter +from vulnerabilities.models import Vulnerability class ProductFilterSet(DataspacedFilterSet): @@ -149,7 +149,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): exploitability = django_filters.ChoiceFilter( label=_("Exploitability"), field_name="package__exploitability", - choices=EXPLOITABILITY_CHOICES, + choices=Vulnerability.EXPLOITABILITY_CHOICES, ) weighted_severity = ScoreRangeFilter( label=_("Severity"), diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html index 32ba9964..d7860ced 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_vulnerabilities.html @@ -31,7 +31,7 @@ - {{ vulnerability.exploitability|default_if_none:"" }} + {{ vulnerability.get_exploitability_display }} {{ vulnerability.weighted_severity|default_if_none:"" }} diff --git a/vulnerabilities/filters.py b/vulnerabilities/filters.py index 763e33d2..e6f627fd 100644 --- a/vulnerabilities/filters.py +++ b/vulnerabilities/filters.py @@ -17,13 +17,6 @@ from dje.widgets import SortDropDownWidget from vulnerabilities.models import Vulnerability -EXPLOITABILITY_CHOICES = [ - (0.5, _("No exploit known (0.5)")), - (1.0, _("Exploit script published (1.0)")), - (2.0, _("High exploitability (2.0)")), -] - - RISK_SCORE_RANGES = { "low": (0.1, 2.9), "medium": (3.0, 5.9), @@ -97,10 +90,6 @@ class VulnerabilityFilterSet(DataspacedFilterSet): ], widget=SortDropDownWidget, ) - exploitability = django_filters.ChoiceFilter( - label=_("Exploitability"), - choices=EXPLOITABILITY_CHOICES, - ) weighted_severity = ScoreRangeFilter( label=_("Severity"), score_ranges=RISK_SCORE_RANGES, @@ -114,6 +103,7 @@ class Meta: model = Vulnerability fields = [ "q", + "exploitability", ] def __init__(self, *args, **kwargs): diff --git a/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py b/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py new file mode 100644 index 00000000..e70208a3 --- /dev/null +++ b/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py @@ -0,0 +1,36 @@ +# Generated by Django 5.0.9 on 2024-11-15 06:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('vulnerabilities', '0001_initial'), + ] + + operations = [ + migrations.RemoveField( + model_name='vulnerability', + name='max_score', + ), + migrations.RemoveField( + model_name='vulnerability', + name='min_score', + ), + migrations.AddField( + model_name='vulnerability', + name='exploitability', + field=models.DecimalField(blank=True, choices=[(0.5, 'No exploits known'), (1.0, 'Potential exploits'), (2.0, 'Known exploits')], decimal_places=1, help_text='Exploitability refers to the potential or probability of a software package vulnerability being exploited by malicious actors to compromise systems, applications, or networks. It is determined automatically by discovery of exploits.', max_digits=3, null=True), + ), + migrations.AddField( + model_name='vulnerability', + name='risk_score', + field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score from 0.0 to 10.0, with higher values indicating greater vulnerability risk. This score is the maximum of the weighted severity multiplied by exploitability, capped at 10.', max_digits=3, null=True), + ), + migrations.AddField( + model_name='vulnerability', + name='weighted_severity', + field=models.DecimalField(blank=True, decimal_places=1, help_text='Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.', max_digits=3, null=True), + ), + ] diff --git a/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py b/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py deleted file mode 100644 index cb769202..00000000 --- a/vulnerabilities/migrations/0002_vulnerability_exploitability_and_more.py +++ /dev/null @@ -1,36 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-12 08:02 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='vulnerability', - name='exploitability', - field=models.DecimalField(decimal_places=2, help_text='Exploitability indicates the likelihood that a vulnerability in a software package could be used by malicious actors to compromise systems, applications, or networks. This metric is determined automatically based on the discovery of known exploits.', max_digits=4, null=True), - ), - migrations.AddField( - model_name='vulnerability', - name='risk_score', - field=models.DecimalField(decimal_places=2, help_text='Risk score from 0.00 to 10.00, with higher values indicating greater vulnerability risk. This score is the maximum of the weighted severity multiplied by exploitability, capped at 10.', max_digits=4, null=True), - ), - migrations.AddField( - model_name='vulnerability', - name='weighted_severity', - field=models.DecimalField(decimal_places=2, help_text='Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.', max_digits=4, null=True), - ), - migrations.RemoveField( - model_name='vulnerability', - name='max_score', - ), - migrations.RemoveField( - model_name='vulnerability', - name='min_score', - ), - ] diff --git a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py index 8ff2cdfc..f8eb82b8 100644 --- a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py +++ b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py @@ -24,7 +24,7 @@ def set_random_risk_score(apps, schema_editor): class Migration(migrations.Migration): dependencies = [ - ('vulnerabilities', '0002_vulnerability_exploitability_and_more'), + ('vulnerabilities', '0002_remove_vulnerability_max_score_and_more'), ] operations = [ diff --git a/vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py b/vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py deleted file mode 100644 index 87070fe0..00000000 --- a/vulnerabilities/migrations/0004_alter_vulnerability_exploitability_and_more.py +++ /dev/null @@ -1,28 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-14 12:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0003_to_delete_temp_fake_values'), - ] - - operations = [ - migrations.AlterField( - model_name='vulnerability', - name='exploitability', - field=models.DecimalField(blank=True, decimal_places=2, help_text='Exploitability indicates the likelihood that a vulnerability in a software package could be used by malicious actors to compromise systems, applications, or networks. This metric is determined automatically based on the discovery of known exploits.', max_digits=4, null=True), - ), - migrations.AlterField( - model_name='vulnerability', - name='risk_score', - field=models.DecimalField(blank=True, decimal_places=2, help_text='Risk score from 0.00 to 10.00, with higher values indicating greater vulnerability risk. This score is the maximum of the weighted severity multiplied by exploitability, capped at 10.', max_digits=4, null=True), - ), - migrations.AlterField( - model_name='vulnerability', - name='weighted_severity', - field=models.DecimalField(blank=True, decimal_places=2, help_text='Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.', max_digits=4, null=True), - ), - ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index af17b1ad..264763af 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -101,23 +101,29 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): output_field=models.IntegerField(), db_persist=True, ) + EXPLOITABILITY_CHOICES = [ + (0.5, _("No exploits known")), + (1.0, _("Potential exploits")), + (2.0, _("Known exploits")), + ] exploitability = models.DecimalField( null=True, blank=True, - max_digits=4, - decimal_places=2, + max_digits=3, + decimal_places=1, + choices=EXPLOITABILITY_CHOICES, help_text=_( - "Exploitability indicates the likelihood that a vulnerability in a " - "software package could be used by malicious actors to compromise systems, " - "applications, or networks. This metric is determined automatically based " - "on the discovery of known exploits." + "Exploitability refers to the potential or probability of a software " + "package vulnerability being exploited by malicious actors to compromise " + "systems, applications, or networks. " + "It is determined automatically by discovery of exploits." ), ) weighted_severity = models.DecimalField( null=True, blank=True, - max_digits=4, - decimal_places=2, + max_digits=3, + decimal_places=1, help_text=_( "Weighted severity is the highest value calculated by multiplying each " "severity by its corresponding weight, divided by 10." @@ -126,10 +132,10 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): risk_score = models.DecimalField( null=True, blank=True, - max_digits=4, - decimal_places=2, + max_digits=3, + decimal_places=1, help_text=_( - "Risk score from 0.00 to 10.00, with higher values indicating greater " + "Risk score from 0.0 to 10.0, with higher values indicating greater " "vulnerability risk. " "This score is the maximum of the weighted severity multiplied by " "exploitability, capped at 10." @@ -367,8 +373,8 @@ class AffectedByVulnerabilityMixin(models.Model): risk_score = models.DecimalField( null=True, blank=True, - max_digits=4, - decimal_places=2, + max_digits=3, + decimal_places=1, help_text=_( "Risk score between 0.00 and 10.00, where higher values " "indicate greater vulnerability risk for the package." diff --git a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html index 4163a641..31ce7a20 100644 --- a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html +++ b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html @@ -36,7 +36,7 @@ {% endif %} - {{ vulnerability.exploitability|default_if_none:"" }} + {{ vulnerability.get_exploitability_display }} {{ vulnerability.weighted_severity|default_if_none:"" }} From 666ef3d9c0fdcf610d1b043f45d1cce223de7b0b Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 15 Nov 2024 11:39:10 +0400 Subject: [PATCH 21/31] Refine the display of exploitability #98 Signed-off-by: tdruez --- .../component_catalog/tabs/tab_vulnerabilities.html | 4 ++-- .../product_portfolio/tabs/tab_inventory.html | 2 +- .../product_portfolio/tabs/tab_vulnerabilities.html | 6 +++--- .../vulnerabilities/includes/exploitability.html | 10 ++++++++++ .../vulnerabilities}/includes/risk_score_badge.html | 0 .../tables/vulnerability_list_table.html | 4 ++-- 6 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 vulnerabilities/templates/vulnerabilities/includes/exploitability.html rename {component_catalog/templates/component_catalog => vulnerabilities/templates/vulnerabilities}/includes/risk_score_badge.html (100%) diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 379957a2..6a920aa4 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -75,13 +75,13 @@ {% endif %} - {{ vulnerability.get_exploitability_display }} + {% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %} {{ vulnerability.weighted_severity|default_if_none:"" }} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} {% if vulnerability.fixed_packages_html %} diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html index 2318448c..436b80fe 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html @@ -134,7 +134,7 @@ {% if product.dataspace.enable_vulnerablecodedb_access %} {% if relation.related_component_or_package.vulnerability_count %} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score only %} + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=relation.related_component_or_package.risk_score only %} {% endif %} - {{ vulnerability.get_exploitability_display }} + {% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %} {{ vulnerability.weighted_severity|default_if_none:"" }} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} {% if vulnerability.summary %} diff --git a/vulnerabilities/templates/vulnerabilities/includes/exploitability.html b/vulnerabilities/templates/vulnerabilities/includes/exploitability.html new file mode 100644 index 00000000..a121db6f --- /dev/null +++ b/vulnerabilities/templates/vulnerabilities/includes/exploitability.html @@ -0,0 +1,10 @@ +{% if instance.exploitability %} + + {{ instance.get_exploitability_display }} + +{% endif %} \ No newline at end of file diff --git a/component_catalog/templates/component_catalog/includes/risk_score_badge.html b/vulnerabilities/templates/vulnerabilities/includes/risk_score_badge.html similarity index 100% rename from component_catalog/templates/component_catalog/includes/risk_score_badge.html rename to vulnerabilities/templates/vulnerabilities/includes/risk_score_badge.html diff --git a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html index 31ce7a20..4b5aae94 100644 --- a/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html +++ b/vulnerabilities/templates/vulnerabilities/tables/vulnerability_list_table.html @@ -36,13 +36,13 @@ {% endif %} - {{ vulnerability.get_exploitability_display }} + {% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %} {{ vulnerability.weighted_severity|default_if_none:"" }} - {% include 'component_catalog/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %} {% if vulnerability.affected_products_count %} From 44580294bd24e5fdeb98057071ef1a7bdd0e44a2 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 15 Nov 2024 15:57:07 +0400 Subject: [PATCH 22/31] Update risk_score help_text #98 Signed-off-by: tdruez --- ...013_alter_component_risk_score_alter_package_risk_score.py | 4 ++-- .../templates/component_catalog/tabs/tab_vulnerabilities.html | 2 +- vulnerabilities/models.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py b/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py index bca35e6e..f102d4e1 100644 --- a/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py +++ b/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py @@ -13,11 +13,11 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='component', name='risk_score', - field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True), + 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.AlterField( model_name='package', name='risk_score', - field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True), + 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/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 6a920aa4..b2d85ab5 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -1,7 +1,7 @@ {% load i18n %}
    - + Risk score
    diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 264763af..8fc0b34b 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -376,7 +376,7 @@ class AffectedByVulnerabilityMixin(models.Model): max_digits=3, decimal_places=1, help_text=_( - "Risk score between 0.00 and 10.00, where higher values " + "Risk score between 0.0 and 10.0, where higher values " "indicate greater vulnerability risk for the package." ), ) From 83fe8cd79f7016e1f190600cacb41f0b45c05172 Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 15 Nov 2024 16:02:18 +0400 Subject: [PATCH 23/31] Display the risk_score as badge in Package vulnerabilities tab #98 Signed-off-by: tdruez --- .../component_catalog/tabs/tab_vulnerabilities.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index b2d85ab5..88d76dcd 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -1,12 +1,12 @@ {% load i18n %}
    -
    +
    Risk score
    -
    -
    {{ package.risk_score }}
    +
    + {% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=package.risk_score only %}
    From c31c4c8f617314d51bea16e801d2079352dfe63a Mon Sep 17 00:00:00 2001 From: tdruez Date: Fri, 15 Nov 2024 16:35:28 +0400 Subject: [PATCH 24/31] Update the risk_score on package in fetch_for_packages #98 Signed-off-by: tdruez --- vulnerabilities/fetch.py | 3 +++ .../tests/data/vulnerabilities/idna_3.6_response.json | 4 ++++ vulnerabilities/tests/test_fetch.py | 7 +++++++ 3 files changed, 14 insertions(+) diff --git a/vulnerabilities/fetch.py b/vulnerabilities/fetch.py index e17a244c..5af3b3c4 100644 --- a/vulnerabilities/fetch.py +++ b/vulnerabilities/fetch.py @@ -90,4 +90,7 @@ def fetch_for_packages(queryset, dataspace, batch_size=50, timeout=None, log_fun created_vulnerabilities += 1 vulnerability.add_affected_packages(affected_packages) + if package_risk_score := vc_entry.get("risk_score"): + affected_packages.update(risk_score=package_risk_score) + return created_vulnerabilities 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 1f79055e..25a264e0 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 @@ -60,3 +61,9 @@ def test_vulnerabilities_fetch_for_packages(self, mock_bulk_search_by_purl): 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) From 7ce941b7f2a68a2ee0c2b4a32a291dcff1f54142 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 18 Nov 2024 10:23:01 +0400 Subject: [PATCH 25/31] Set proper max_digits to 2 for exploitability #98 Signed-off-by: tdruez --- .../templates/component_catalog/tabs/tab_vulnerabilities.html | 2 +- .../migrations/0002_remove_vulnerability_max_score_and_more.py | 2 +- vulnerabilities/models.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html index 88d76dcd..9fa439bd 100644 --- a/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html +++ b/component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html @@ -33,7 +33,7 @@ diff --git a/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py b/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py index e70208a3..43562e46 100644 --- a/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py +++ b/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py @@ -21,7 +21,7 @@ class Migration(migrations.Migration): migrations.AddField( model_name='vulnerability', name='exploitability', - field=models.DecimalField(blank=True, choices=[(0.5, 'No exploits known'), (1.0, 'Potential exploits'), (2.0, 'Known exploits')], decimal_places=1, help_text='Exploitability refers to the potential or probability of a software package vulnerability being exploited by malicious actors to compromise systems, applications, or networks. It is determined automatically by discovery of exploits.', max_digits=3, null=True), + field=models.DecimalField(blank=True, choices=[(0.5, 'No exploits known'), (1.0, 'Potential exploits'), (2.0, 'Known exploits')], decimal_places=1, help_text='Exploitability refers to the potential or probability of a software package vulnerability being exploited by malicious actors to compromise systems, applications, or networks. It is determined automatically by discovery of exploits.', max_digits=2, null=True), ), migrations.AddField( model_name='vulnerability', diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 8fc0b34b..726801f9 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -109,7 +109,7 @@ class Vulnerability(HistoryDateFieldsMixin, DataspacedModel): exploitability = models.DecimalField( null=True, blank=True, - max_digits=3, + max_digits=2, decimal_places=1, choices=EXPLOITABILITY_CHOICES, help_text=_( From 6f4f3d1514de8a52a1cacba4255b9bcb0dd78d8f Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 18 Nov 2024 10:31:20 +0400 Subject: [PATCH 26/31] Fix issue with the new filters #98 Signed-off-by: tdruez --- product_portfolio/filters.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/product_portfolio/filters.py b/product_portfolio/filters.py index 9764c6b7..7a9785bc 100644 --- a/product_portfolio/filters.py +++ b/product_portfolio/filters.py @@ -122,6 +122,7 @@ class Meta: class BaseProductRelationFilterSet(DataspacedFilterSet): + field_name_prefix = None is_deployed = BooleanChoiceFilter( empty_label="All (Inventory)", choices=( @@ -148,17 +149,14 @@ class BaseProductRelationFilterSet(DataspacedFilterSet): ) exploitability = django_filters.ChoiceFilter( label=_("Exploitability"), - field_name="package__exploitability", choices=Vulnerability.EXPLOITABILITY_CHOICES, ) weighted_severity = ScoreRangeFilter( label=_("Severity"), - field_name="package__weighted_severity", score_ranges=RISK_SCORE_RANGES, ) risk_score = ScoreRangeFilter( label=_("Risk score"), - field_name="package__risk_score", score_ranges=RISK_SCORE_RANGES, ) @@ -192,12 +190,15 @@ def __init__(self, *args, **kwargs): anchor=self.anchor, right_align=True ) - self.filters["exploitability"].extra["widget"] = DropDownWidget(anchor=self.anchor) - self.filters["weighted_severity"].extra["widget"] = DropDownWidget(anchor=self.anchor) - self.filters["risk_score"].extra["widget"] = DropDownWidget(anchor=self.anchor) + 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=[ @@ -246,6 +247,7 @@ class Meta: class ProductPackageFilterSet(BaseProductRelationFilterSet): + field_name_prefix = "package" q = SearchFilter( label=_("Search"), search_fields=[ @@ -285,10 +287,6 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet): ), ) - @staticmethod - def do_nothing(queryset, name, value): - return queryset - class Meta: model = ProductPackage fields = [ From e228710a1cdce520dfc4ef96112637e0f47a9166 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 18 Nov 2024 10:32:22 +0400 Subject: [PATCH 27/31] Render exploitability as a badge #98 Signed-off-by: tdruez --- .../templates/product_portfolio/tabs/tab_inventory.html | 2 +- .../vulnerabilities/includes/exploitability.html | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html index 436b80fe..386dfd41 100644 --- a/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html +++ b/product_portfolio/templates/product_portfolio/tabs/tab_inventory.html @@ -56,7 +56,7 @@ {% trans 'Concluded license' %} -
    - + {% trans 'Risk' %} + {% trans 'Compliance status' %} diff --git a/vulnerabilities/templates/vulnerabilities/includes/exploitability.html b/vulnerabilities/templates/vulnerabilities/includes/exploitability.html index a121db6f..be644eba 100644 --- a/vulnerabilities/templates/vulnerabilities/includes/exploitability.html +++ b/vulnerabilities/templates/vulnerabilities/includes/exploitability.html @@ -1,8 +1,9 @@ {% if instance.exploitability %} - {{ instance.get_exploitability_display }} From 7358a09c8929bcd0b9cc42f9c57eabd6d6126c25 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 18 Nov 2024 13:59:28 +0400 Subject: [PATCH 28/31] Add unit test for the risk_score filter in Inventory tab #98 Signed-off-by: tdruez --- product_portfolio/tests/test_views.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/product_portfolio/tests/test_views.py b/product_portfolio/tests/test_views.py index a378b1e7..7c2e8fcc 100644 --- a/product_portfolio/tests/test_views.py +++ b/product_portfolio/tests/test_views.py @@ -341,8 +341,11 @@ def test_product_portfolio_detail_view_object_type_filter_in_inventory_tab(self) pc2_custom = ProductComponent.objects.create( product=self.product1, name="temporary name", is_modified=True, dataspace=self.dataspace ) + self.package1.update(risk_score=1.0) pp1 = ProductPackage.objects.create( - product=self.product1, package=self.package1, dataspace=self.dataspace + product=self.product1, + package=self.package1, + dataspace=self.dataspace, ) response = self.client.get(self.product1.get_absolute_url()) @@ -405,6 +408,12 @@ def test_product_portfolio_detail_view_object_type_filter_in_inventory_tab(self) self.assertIn(pc2_custom, pc_filterset) self.assertNotIn(pp1, pc_filterset) + response = self.client.get(url + "?inventory-risk_score=low") + pc_filterset = response.context["inventory_items"][""] + self.assertNotIn(pc_valid, pc_filterset) + self.assertNotIn(pc2_custom, pc_filterset) + self.assertIn(pp1, pc_filterset) + def test_product_portfolio_detail_view_review_status_filter_in_inventory_tab(self): self.client.login(username="nexb_user", password="secret") From 883bf95bb8f19b3745de0d3302eb5bb8edadebc9 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 18 Nov 2024 14:02:35 +0400 Subject: [PATCH 29/31] Consolidate migration files #98 Signed-off-by: tdruez --- ...component_risk_score_package_risk_score.py | 6 ++-- .../0011_to_delete_temp_fake_values.py | 34 ------------------- ...ent_risk_score_alter_package_risk_score.py | 23 ------------- ...ent_risk_score_alter_package_risk_score.py | 23 ------------- ...remove_vulnerability_max_score_and_more.py | 2 +- .../0003_to_delete_temp_fake_values.py | 32 ----------------- 6 files changed, 4 insertions(+), 116 deletions(-) delete mode 100644 component_catalog/migrations/0011_to_delete_temp_fake_values.py delete mode 100644 component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py delete mode 100644 component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py delete mode 100644 vulnerabilities/migrations/0003_to_delete_temp_fake_values.py 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 index 614567dd..5935333d 100644 --- a/component_catalog/migrations/0010_component_risk_score_package_risk_score.py +++ b/component_catalog/migrations/0010_component_risk_score_package_risk_score.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.9 on 2024-11-08 13:24 +# Generated by Django 5.0.9 on 2024-11-18 10:01 from django.db import migrations, models @@ -13,11 +13,11 @@ class Migration(migrations.Migration): migrations.AddField( model_name='component', name='risk_score', - field=models.DecimalField(decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), + 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(decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), + 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/migrations/0011_to_delete_temp_fake_values.py b/component_catalog/migrations/0011_to_delete_temp_fake_values.py deleted file mode 100644 index 4999d7ef..00000000 --- a/component_catalog/migrations/0011_to_delete_temp_fake_values.py +++ /dev/null @@ -1,34 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-12 08:13 - -from django.db import migrations -from django.db.models import Count - - -def generate_random_risk_score(): - import random - from decimal import Decimal - - return Decimal(f"{random.uniform(0, 10):.2f}") - - -def set_random_risk_score(apps, schema_editor): - Package = apps.get_model("component_catalog", "Package") - - qs = Package.objects.annotate( - vulnerability_count=Count("affected_by_vulnerabilities", distinct=True) - ).filter(vulnerability_count__gt=0) - - for package in qs: - risk_score = generate_random_risk_score() - Package.objects.filter(pk=package.pk).update(risk_score=risk_score) - - -class Migration(migrations.Migration): - - dependencies = [ - ('component_catalog', '0010_component_risk_score_package_risk_score'), - ] - - operations = [ - migrations.RunPython(set_random_risk_score, reverse_code=migrations.RunPython.noop), - ] diff --git a/component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py b/component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py deleted file mode 100644 index f11c7c43..00000000 --- a/component_catalog/migrations/0012_alter_component_risk_score_alter_package_risk_score.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-14 12:08 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('component_catalog', '0011_to_delete_temp_fake_values'), - ] - - operations = [ - migrations.AlterField( - model_name='component', - name='risk_score', - field=models.DecimalField(blank=True, decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), - ), - migrations.AlterField( - model_name='package', - name='risk_score', - field=models.DecimalField(blank=True, decimal_places=2, help_text='Risk score between 0.00 and 10.00, where higher values indicate greater vulnerability risk for the package.', max_digits=4, null=True), - ), - ] diff --git a/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py b/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py deleted file mode 100644 index f102d4e1..00000000 --- a/component_catalog/migrations/0013_alter_component_risk_score_alter_package_risk_score.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-15 06:27 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('component_catalog', '0012_alter_component_risk_score_alter_package_risk_score'), - ] - - operations = [ - migrations.AlterField( - 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.AlterField( - 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/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py b/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py index 43562e46..8fa9adfc 100644 --- a/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py +++ b/vulnerabilities/migrations/0002_remove_vulnerability_max_score_and_more.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.9 on 2024-11-15 06:18 +# Generated by Django 5.0.9 on 2024-11-18 10:01 from django.db import migrations, models diff --git a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py b/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py deleted file mode 100644 index f8eb82b8..00000000 --- a/vulnerabilities/migrations/0003_to_delete_temp_fake_values.py +++ /dev/null @@ -1,32 +0,0 @@ -# Generated by Django 5.0.9 on 2024-11-12 08:13 - -from django.db import migrations -import random -from decimal import Decimal - - -def set_random_risk_score(apps, schema_editor): - Vulnerability = apps.get_model("vulnerabilities", "Vulnerability") - - qs = Vulnerability.objects.all() - for vulnerability in qs: - exploitability = random.choice([0.5, 1.0, 2.0]) # 0.5, 1.0, or 2.0 - weighted_severity = Decimal(f"{random.uniform(0, 10):.2f}") # 0 to 10 - risk_score = f"{min(float(exploitability) * float(weighted_severity), 10.0):.2f}" - - Vulnerability.objects.filter(pk=vulnerability.pk).update( - exploitability=exploitability, - weighted_severity=weighted_severity, - risk_score=risk_score, - ) - - -class Migration(migrations.Migration): - - dependencies = [ - ('vulnerabilities', '0002_remove_vulnerability_max_score_and_more'), - ] - - operations = [ - migrations.RunPython(set_random_risk_score, reverse_code=migrations.RunPython.noop), - ] From e0d5f66cbf55cf7b51a62d9cab8c855babea6b61 Mon Sep 17 00:00:00 2001 From: tdruez Date: Mon, 18 Nov 2024 17:41:51 +0400 Subject: [PATCH 30/31] Add changelog entry Signed-off-by: tdruez --- CHANGELOG.rst | 5 +++++ 1 file changed, 5 insertions(+) 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. From 78f30e83357f734b9ebd1d8e8dd9d0326f14a5bf Mon Sep 17 00:00:00 2001 From: tdruez Date: Wed, 20 Nov 2024 12:56:11 +0400 Subject: [PATCH 31/31] Add support for update in fetch_for_packages #98 Signed-off-by: tdruez --- component_catalog/tests/test_models.py | 4 ++++ dje/models.py | 12 ++++++++-- dje/tasks.py | 2 +- vulnerabilities/fetch.py | 23 ++++++++++++++++--- .../commands/fetchvulnerabilities.py | 8 ++++++- vulnerabilities/tests/test_fetch.py | 16 +++++++++++-- 6 files changed, 56 insertions(+), 9 deletions(-) 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/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/vulnerabilities/fetch.py b/vulnerabilities/fetch.py index 5af3b3c4..749f6b00 100644 --- a/vulnerabilities/fetch.py +++ b/vulnerabilities/fetch.py @@ -20,7 +20,7 @@ from vulnerabilities.models import Vulnerability -def fetch_from_vulnerablecode(dataspace, batch_size, timeout, log_func=None): +def fetch_from_vulnerablecode(dataspace, batch_size, update, timeout, log_func=None): start_time = timer() vulnerablecode = VulnerableCode(dataspace) if not vulnerablecode.is_configured(): @@ -38,7 +38,14 @@ def fetch_from_vulnerablecode(dataspace, batch_size, timeout, log_func=None): if log_func: log_func(f"{package_count} Packages in the queue.") - created = fetch_for_packages(package_qs, dataspace, batch_size, timeout, log_func) + created = fetch_for_packages( + queryset=package_qs, + dataspace=dataspace, + batch_size=batch_size, + update=update, + timeout=timeout, + log_func=log_func, + ) run_time = timer() - start_time if log_func: log_func(f"+ Created {intcomma(created)} vulnerabilities") @@ -48,7 +55,9 @@ def fetch_from_vulnerablecode(dataspace, batch_size, timeout, log_func=None): dataspace.save(update_fields=["vulnerabilities_updated_at"]) -def fetch_for_packages(queryset, dataspace, batch_size=50, timeout=None, log_func=None): +def fetch_for_packages( + queryset, dataspace, batch_size=50, update=True, timeout=None, log_func=None +): object_count = queryset.count() if object_count < 1: return @@ -56,6 +65,7 @@ def fetch_for_packages(queryset, dataspace, batch_size=50, timeout=None, log_fun vulnerablecode = VulnerableCode(dataspace) vulnerability_qs = Vulnerability.objects.scope(dataspace) created_vulnerabilities = 0 + updated_vulnerabilities = 0 for index, batch in enumerate(chunked_queryset(queryset, batch_size), start=1): if log_func: @@ -88,6 +98,13 @@ def fetch_for_packages(queryset, dataspace, batch_size=50, timeout=None, log_fun data=vulnerability_data, ) created_vulnerabilities += 1 + elif update: + updated_fields = vulnerability.update_from_data( + user=None, data=vulnerability_data, override=True + ) + if updated_fields: + updated_vulnerabilities += 1 + vulnerability.add_affected_packages(affected_packages) if package_risk_score := vc_entry.get("risk_score"): diff --git a/vulnerabilities/management/commands/fetchvulnerabilities.py b/vulnerabilities/management/commands/fetchvulnerabilities.py index 741be62d..cb8f22bb 100644 --- a/vulnerabilities/management/commands/fetchvulnerabilities.py +++ b/vulnerabilities/management/commands/fetchvulnerabilities.py @@ -43,4 +43,10 @@ def handle(self, *args, **options): if not vulnerablecode.is_configured(): raise CommandError("VulnerableCode is not configured.") - fetch_from_vulnerablecode(self.dataspace, batch_size, timeout, log_func=self.stdout.write) + fetch_from_vulnerablecode( + self.dataspace, + batch_size=batch_size, + update=True, + timeout=timeout, + log_func=self.stdout.write, + ) diff --git a/vulnerabilities/tests/test_fetch.py b/vulnerabilities/tests/test_fetch.py index 25a264e0..47a105d8 100644 --- a/vulnerabilities/tests/test_fetch.py +++ b/vulnerabilities/tests/test_fetch.py @@ -37,7 +37,9 @@ def test_vulnerabilities_fetch_from_vulnerablecode( make_package(self.dataspace, package_url="pkg:pypi/idna@2.0") mock_is_configured.return_value = True mock_fetch_for_packages.return_value = 2 - fetch_from_vulnerablecode(self.dataspace, batch_size=1, timeout=None, log_func=buffer.write) + 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() @@ -54,7 +56,7 @@ def test_vulnerabilities_fetch_for_packages(self, mock_bulk_search_by_purl): mock_bulk_search_by_purl.return_value = response_json["results"] created_vulnerabilities = fetch_for_packages( - queryset, self.dataspace, batch_size=1, log_func=buffer.write + 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()) @@ -67,3 +69,13 @@ def test_vulnerabilities_fetch_for_packages(self, mock_bulk_search_by_purl): 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)