Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store and display new Package.risk_score field in the UI #194

Merged
merged 32 commits into from
Nov 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
9882b52
Add a risk_score field and display the values in the UI #98
tdruez Nov 8, 2024
97e1bb9
Generate random value for the risk_score #98
tdruez Nov 8, 2024
bbad3da
Remove temp data migration #98
tdruez Nov 11, 2024
b90cf9d
CSS adjustments #98
tdruez Nov 11, 2024
57d5acd
Rename fetch_for_queryset to fetch_for_packages #98
tdruez Nov 11, 2024
276ac5b
Add exploitability, weighted_severity, risk_score on Vulnerability #98
tdruez Nov 12, 2024
b72c6c9
Display new fields in Vulnerabilities tab #98
tdruez Nov 12, 2024
c0af3df
Display new fields in Vulnerabilities lists #98
tdruez Nov 12, 2024
f281f13
Refine the Risk badge rendering #98
tdruez Nov 12, 2024
7d5b4a6
Add filter for all new fields #98
tdruez Nov 12, 2024
4f37c7b
Sort the vulnerability by risk in listing #98
tdruez Nov 12, 2024
dba5806
Remove the min_score and max_score attributes from model #98
tdruez Nov 12, 2024
b727596
Add help text in Inventory tab headers #98
tdruez Nov 12, 2024
0bf3a1c
Remove dead code #98
tdruez Nov 14, 2024
bef324e
Set proper choices for the exploitability filter #98
tdruez Nov 14, 2024
ed37abe
Consolidate migrations #98
tdruez Nov 14, 2024
750b65a
Fix part of the failing tests #98
tdruez Nov 14, 2024
5f82ebb
Add migration files #98
tdruez Nov 14, 2024
8aa4cec
Fix failing tests #98
tdruez Nov 14, 2024
1bd041a
Refine the DecimalField and exploitability label system #98
tdruez Nov 15, 2024
666ef3d
Refine the display of exploitability #98
tdruez Nov 15, 2024
4458029
Update risk_score help_text #98
tdruez Nov 15, 2024
83fe8cd
Display the risk_score as badge in Package vulnerabilities tab #98
tdruez Nov 15, 2024
c31c4c8
Update the risk_score on package in fetch_for_packages #98
tdruez Nov 15, 2024
7ce941b
Set proper max_digits to 2 for exploitability #98
tdruez Nov 18, 2024
6f4f3d1
Fix issue with the new filters #98
tdruez Nov 18, 2024
e228710
Render exploitability as a badge #98
tdruez Nov 18, 2024
7358a09
Add unit test for the risk_score filter in Inventory tab #98
tdruez Nov 18, 2024
883bf95
Consolidate migration files #98
tdruez Nov 18, 2024
8ae3b78
Merge branch 'main' into 98-risk-score
tdruez Nov 18, 2024
e0d5f66
Add changelog entry
tdruez Nov 18, 2024
78f30e8
Add support for update in fetch_for_packages #98
tdruez Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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