Skip to content

Commit

Permalink
Store and display new Package.risk_score field in the UI (#194)
Browse files Browse the repository at this point in the history
* Add a risk_score field and display the values in the UI #98

Signed-off-by: tdruez <[email protected]>

* Generate random value for the risk_score #98

Signed-off-by: tdruez <[email protected]>

* Remove temp data migration #98

Signed-off-by: tdruez <[email protected]>

* CSS adjustments #98

Signed-off-by: tdruez <[email protected]>

* Rename fetch_for_queryset to fetch_for_packages #98

Signed-off-by: tdruez <[email protected]>

* Add exploitability, weighted_severity, risk_score on Vulnerability #98

Signed-off-by: tdruez <[email protected]>

* Display new fields in Vulnerabilities tab #98

Signed-off-by: tdruez <[email protected]>

* Display new fields in Vulnerabilities lists #98

Signed-off-by: tdruez <[email protected]>

* Refine the Risk badge rendering #98

Signed-off-by: tdruez <[email protected]>

* Add filter for all new fields #98

Signed-off-by: tdruez <[email protected]>

* Sort the vulnerability by risk in listing #98

Signed-off-by: tdruez <[email protected]>

* Remove the min_score and max_score attributes from model #98

Signed-off-by: tdruez <[email protected]>

* Add help text in Inventory tab headers #98

Signed-off-by: tdruez <[email protected]>

* Remove dead code #98

Signed-off-by: tdruez <[email protected]>

* Set proper choices for the exploitability filter #98

Signed-off-by: tdruez <[email protected]>

* Consolidate migrations #98

Signed-off-by: tdruez <[email protected]>

* Fix part of the failing tests #98

Signed-off-by: tdruez <[email protected]>

* Add migration files #98

Signed-off-by: tdruez <[email protected]>

* Fix failing tests #98

Signed-off-by: tdruez <[email protected]>

* Refine the DecimalField and exploitability label system #98

Signed-off-by: tdruez <[email protected]>

* Refine the display of exploitability #98

Signed-off-by: tdruez <[email protected]>

* Update risk_score help_text #98

Signed-off-by: tdruez <[email protected]>

* Display the risk_score as badge in Package vulnerabilities tab #98

Signed-off-by: tdruez <[email protected]>

* Update the risk_score on package in fetch_for_packages #98

Signed-off-by: tdruez <[email protected]>

* Set proper max_digits to 2 for exploitability #98

Signed-off-by: tdruez <[email protected]>

* Fix issue with the new filters #98

Signed-off-by: tdruez <[email protected]>

* Render exploitability as a badge #98

Signed-off-by: tdruez <[email protected]>

* Add unit test for the risk_score filter in Inventory tab #98

Signed-off-by: tdruez <[email protected]>

* Consolidate migration files #98

Signed-off-by: tdruez <[email protected]>

* Add changelog entry

Signed-off-by: tdruez <[email protected]>

* Add support for update in fetch_for_packages #98

Signed-off-by: tdruez <[email protected]>

---------

Signed-off-by: tdruez <[email protected]>
  • Loading branch information
tdruez authored Nov 20, 2024
1 parent 219b5e6 commit 17a5006
Show file tree
Hide file tree
Showing 34 changed files with 468 additions and 221 deletions.
5 changes: 5 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 2 additions & 2 deletions component_catalog/importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Generated by Django 5.0.9 on 2024-11-18 10:01

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('component_catalog', '0009_componentaffectedbyvulnerability_and_more'),
]

operations = [
migrations.AddField(
model_name='component',
name='risk_score',
field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.0 and 10.0, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True),
),
migrations.AddField(
model_name='package',
name='risk_score',
field=models.DecimalField(blank=True, decimal_places=1, help_text='Risk score between 0.0 and 10.0, where higher values indicate greater vulnerability risk for the package.', max_digits=3, null=True),
),
]
1 change: 1 addition & 0 deletions component_catalog/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
{% load i18n %}
<dl class="row mb-3">
<dt class="col-sm-1 text-end pe-0">
<span class="help_text" data-bs-placement="right" data-bs-toggle="tooltip" data-bs-title="Risk score between 0.0 and 10.0, where higher values indicate greater vulnerability risk for the package.">
Risk score
</span>
</dt>
<dd class="col-sm-11 fs-110pct">
{% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=package.risk_score only %}
</dd>
</dl>
<table class="table table-bordered table-hover table-md text-break">
<thead>
<tr>
Expand All @@ -7,19 +17,24 @@
{% trans 'Affected by' %}
</span>
</th>
<th style="width: 210px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="A list of aliases for this vulnerability.">
{% trans 'Aliases' %}
<th style="width: 300px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Summary of the vulnerability.">
{% trans 'Summary' %}
</span>
</th>
<th style="width: 90px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Severity score range.">
{% trans 'Score' %}
<th style="min-width: 130px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="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">
{% trans 'Exploitability' %}
</span>
</th>
<th>
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Summary of the vulnerability.">
{% trans 'Summary' %}
<th style="min-width: 100px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="Weighted severity is the highest value calculated by multiplying each severity by its corresponding weight, divided by 10.">
{% trans 'Severity' %}
</span>
</th>
<th style="min-width: 90px;">
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="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.">
{% trans 'Risk' %}
</span>
</th>
<th style="min-width: 320px;">
Expand All @@ -43,19 +58,9 @@
{{ vulnerability.vulnerability_id }}
{% endif %}
</strong>
</td>
<td>
{% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
</td>
<td>
{% if vulnerability.min_score %}
{{ vulnerability.min_score }} -
{% endif %}
{% if vulnerability.max_score %}
<strong>
{{ vulnerability.max_score }}
</strong>
{% endif %}
<div class="mt-2">
{% include 'component_catalog/includes/vulnerability_aliases.html' with aliases=vulnerability.aliases only %}
</div>
</td>
<td>
{% if vulnerability.summary %}
Expand All @@ -69,6 +74,15 @@
{% endif %}
{% endif %}
</td>
<td>
{% include 'vulnerabilities/includes/exploitability.html' with instance=vulnerability only %}
</td>
<td>
{{ vulnerability.weighted_severity|default_if_none:"" }}
</td>
<td class="fs-110pct">
{% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=vulnerability.risk_score only %}
</td>
<td>
{% if vulnerability.fixed_packages_html %}
{{ vulnerability.fixed_packages_html }}
Expand Down
8 changes: 4 additions & 4 deletions component_catalog/tests/test_importers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1397,17 +1397,17 @@ 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()

file = os.path.join(TESTFILES_LOCATION, "package_from_scancode.json")
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):
Expand Down
4 changes: 4 additions & 0 deletions component_catalog/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
2 changes: 1 addition & 1 deletion component_catalog/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
24 changes: 18 additions & 6 deletions dejacode/static/css/dejacode_bootstrap.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ a.dropdown-item:hover {
.fs-85pct {
font-size: 85%;
}
.fs-110pct {
font-size: 110%;
}
.header {
margin-bottom: 1rem;
}
Expand Down Expand Up @@ -86,6 +89,9 @@ table.text-break thead {
word-wrap: initial!important;
word-break: initial!important;
}
.bg-warning-orange {
background-color: var(--bs-orange);
}
/* -- Dark there fixes -- */
[data-bs-theme=dark] .btn-outline-dark {
--bs-btn-color: var(--bs-tertiary-color);
Expand Down Expand Up @@ -380,14 +386,20 @@ table.vulnerabilities-table .column-summary {
#tab_vulnerabilities .column-vulnerability_id {
width: 210px;
}
#tab_vulnerabilities .column-aliases {
width: 210px;
#tab_vulnerabilities .column-affected_packages {
min-width: 300px;
}
#tab_vulnerabilities .column-max_score {
width: 105px;
#tab_vulnerabilities .column-exploitability {
width: 150px;
}
#tab_vulnerabilities .column-column-affected_packages {
width: 320px;
#tab_vulnerabilities .column-weighted_severity {
width: 115px;
}
#tab_vulnerabilities .column-risk_score {
width: 95px;
}
#tab_vulnerabilities .column-summary {
width: 300px;
}

/* -- Dependency tab -- */
Expand Down
1 change: 1 addition & 0 deletions dje/copier.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
"project_uuid",
"default_assignee",
"affected_by_vulnerabilities",
"risk_score",
]


Expand Down
12 changes: 10 additions & 2 deletions dje/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion dje/tasks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
3 changes: 3 additions & 0 deletions dje/tests/testfiles/test_dataset_cc_only.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": "",
Expand Down Expand Up @@ -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": "",
Expand Down Expand Up @@ -280,6 +282,7 @@
"version": "",
"qualifiers": "",
"subpath": "",
"risk_score": null,
"declared_license_expression": "",
"other_license_expression": "",
"holder": "",
Expand Down
1 change: 1 addition & 0 deletions dje/tests/testfiles/test_dataset_pp_only.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"version": "",
"qualifiers": "",
"subpath": "",
"risk_score": null,
"declared_license_expression": "",
"other_license_expression": "",
"holder": "",
Expand Down
30 changes: 24 additions & 6 deletions product_portfolio/filters.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,9 @@
from product_portfolio.models import ProductDependency
from product_portfolio.models import ProductPackage
from product_portfolio.models import ProductStatus
from vulnerabilities.filters import RISK_SCORE_RANGES
from vulnerabilities.filters import ScoreRangeFilter
from vulnerabilities.models import Vulnerability


class ProductFilterSet(DataspacedFilterSet):
Expand Down Expand Up @@ -119,6 +122,7 @@ class Meta:


class BaseProductRelationFilterSet(DataspacedFilterSet):
field_name_prefix = None
is_deployed = BooleanChoiceFilter(
empty_label="All (Inventory)",
choices=(
Expand All @@ -130,9 +134,7 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
right_align=True,
),
)

is_modified = BooleanChoiceFilter()

object_type = django_filters.CharFilter(
method="filter_object_type",
widget=DropDownWidget(
Expand All @@ -145,6 +147,18 @@ class BaseProductRelationFilterSet(DataspacedFilterSet):
),
),
)
exploitability = django_filters.ChoiceFilter(
label=_("Exploitability"),
choices=Vulnerability.EXPLOITABILITY_CHOICES,
)
weighted_severity = ScoreRangeFilter(
label=_("Severity"),
score_ranges=RISK_SCORE_RANGES,
)
risk_score = ScoreRangeFilter(
label=_("Risk score"),
score_ranges=RISK_SCORE_RANGES,
)

@staticmethod
def filter_object_type(queryset, name, value):
Expand Down Expand Up @@ -176,8 +190,15 @@ def __init__(self, *args, **kwargs):
anchor=self.anchor, right_align=True
)

field_name_prefix = self.field_name_prefix
for field_name in ["exploitability", "weighted_severity", "risk_score"]:
field = self.filters[field_name]
field.extra["widget"] = DropDownWidget(anchor=self.anchor)
field.field_name = f"{field_name_prefix}__{field_name}"


class ProductComponentFilterSet(BaseProductRelationFilterSet):
field_name_prefix = "component"
q = SearchFilter(
label=_("Search"),
search_fields=[
Expand Down Expand Up @@ -226,6 +247,7 @@ class Meta:


class ProductPackageFilterSet(BaseProductRelationFilterSet):
field_name_prefix = "package"
q = SearchFilter(
label=_("Search"),
search_fields=[
Expand Down Expand Up @@ -265,10 +287,6 @@ class ProductPackageFilterSet(BaseProductRelationFilterSet):
),
)

@staticmethod
def do_nothing(queryset, name, value):
return queryset

class Meta:
model = ProductPackage
fields = [
Expand Down
Loading

0 comments on commit 17a5006

Please sign in to comment.