Skip to content

Commit 56ff72e

Browse files
authored
feat: add fixed_by_packages data from VCIO API v3 (#571)
Signed-off-by: tdruez <tdruez@aboutcode.org>
1 parent 0e40aeb commit 56ff72e

27 files changed

Lines changed: 352 additions & 189 deletions

component_catalog/api.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,8 @@ class Meta:
697697
"last_modified_date",
698698
"collect_data",
699699
"risk_score",
700+
"next_non_vulnerable_version",
701+
"latest_non_vulnerable_version",
700702
"affected_by_vulnerabilities",
701703
)
702704
extra_kwargs = {
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Generated by Django 6.0.6 on 2026-08-27 07:41
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
('component_catalog', '0014_add_detected_date_to_affected_by_vulnerability'),
10+
]
11+
12+
operations = [
13+
migrations.AddField(
14+
model_name='package',
15+
name='latest_non_vulnerable_version',
16+
field=models.CharField(blank=True, help_text='The latest available version that is not vulnerable.', max_length=100),
17+
),
18+
migrations.AddField(
19+
model_name='package',
20+
name='next_non_vulnerable_version',
21+
field=models.CharField(blank=True, help_text='The next version, following this one, that is not vulnerable.', max_length=100),
22+
),
23+
]

component_catalog/models.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1799,6 +1799,7 @@ def only_rendering_fields(self):
17991799
"filename",
18001800
"license_expression",
18011801
"risk_score",
1802+
"latest_non_vulnerable_version",
18021803
"dataspace__name",
18031804
"dataspace__show_usage_policy_in_user_views",
18041805
)
@@ -1977,6 +1978,17 @@ class Package(
19771978
related_name="affected_%(class)ss",
19781979
help_text=_("Vulnerabilities affecting this object."),
19791980
)
1981+
# Based on vulnerablecode.vulnerabilities.models.Package
1982+
next_non_vulnerable_version = models.CharField(
1983+
max_length=100,
1984+
blank=True,
1985+
help_text=_("The next version, following this one, that is not vulnerable."),
1986+
)
1987+
latest_non_vulnerable_version = models.CharField(
1988+
max_length=100,
1989+
blank=True,
1990+
help_text=_("The latest available version that is not vulnerable."),
1991+
)
19801992

19811993
objects = DataspacedManager.from_queryset(PackageQuerySet)()
19821994

component_catalog/templates/component_catalog/tabs/tab_vulnerabilities.html

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,28 @@
11
{% load i18n %}
2-
<dl class="row mb-3">
3-
<dt class="col-sm-1 text-end pe-0">
4-
<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.">
2+
<div class="d-flex flex-wrap align-items-center gap-4 mb-3">
3+
<div class="d-flex align-items-center gap-2">
4+
<span class="help_text" 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.">
55
Risk score
66
</span>
7-
</dt>
8-
<dd class="col-sm-11 fs-110pct">
97
{% include 'vulnerabilities/includes/risk_score_badge.html' with risk_score=package.risk_score only %}
10-
</dd>
11-
</dl>
8+
</div>
9+
{% if package.next_non_vulnerable_version %}
10+
<div class="d-flex align-items-center gap-2">
11+
<span class="help_text" data-bs-toggle="tooltip" data-bs-title="The next version, following this one, that is not vulnerable.">
12+
Next non-vulnerable version
13+
</span>
14+
<span class="fw-semibold">{{ package.next_non_vulnerable_version }}</span>
15+
</div>
16+
{% endif %}
17+
{% if package.latest_non_vulnerable_version %}
18+
<div class="d-flex align-items-center gap-2">
19+
<span class="help_text" data-bs-toggle="tooltip" data-bs-title="The latest available version that is not vulnerable.">
20+
Latest non-vulnerable version
21+
</span>
22+
<span class="fw-semibold">{{ package.latest_non_vulnerable_version }}</span>
23+
</div>
24+
{% endif %}
25+
</div>
1226
<table class="table table-bordered table-hover table-md text-break">
1327
<thead>
1428
<tr>
@@ -39,7 +53,7 @@
3953
</th>
4054
<th style="min-width: 320px;">
4155
<span class="help_text" data-bs-toggle="tooltip" data-bs-placement="bottom" data-bs-title="The identifiers of Package Versions that have been reported to fix a specific vulnerability and collected in VulnerableCodeDB.">
42-
{% trans 'Fixed packages' %}
56+
{% trans 'Fixed by packages' %}
4357
</span>
4458
</th>
4559
</tr>

component_catalog/tests/test_api.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1339,7 +1339,11 @@ def test_api_package_endpoint_vulnerabilities_features(self):
13391339
self.client.login(username="super_user", password="secret")
13401340
vulnerability1 = make_vulnerability(self.dataspace, affecting=self.package1)
13411341
vulnerability2 = make_vulnerability(self.dataspace)
1342-
self.package1.update(risk_score=9.0)
1342+
self.package1.update(
1343+
risk_score=9.0,
1344+
next_non_vulnerable_version="1.2.4",
1345+
latest_non_vulnerable_version="2.0.0",
1346+
)
13431347

13441348
data = {"is_vulnerable": "yes"}
13451349
response = self.client.get(self.package_list_url, data)
@@ -1349,6 +1353,8 @@ def test_api_package_endpoint_vulnerabilities_features(self):
13491353

13501354
results = response.data["results"]
13511355
self.assertEqual("9.0", results[0]["risk_score"])
1356+
self.assertEqual("1.2.4", results[0]["next_non_vulnerable_version"])
1357+
self.assertEqual("2.0.0", results[0]["latest_non_vulnerable_version"])
13521358
self.assertEqual(
13531359
vulnerability1.advisory_id,
13541360
results[0]["affected_by_vulnerabilities"][0]["advisory_id"],

component_catalog/tests/test_views.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3023,6 +3023,23 @@ def test_package_details_view_tab_vulnerabilities(self):
30233023
self.assertContains(response, 'id="tab_vulnerabilities"')
30243024
self.assertContains(response, self.vulnerability1.advisory_id)
30253025

3026+
def test_package_details_view_tab_vulnerabilities_fixed_by_packages(self):
3027+
fixing_package = make_package(self.dataspace, package_url="pkg:pypi/idna@3.7")
3028+
self.vulnerability1.fixed_by_packages = [
3029+
"pkg:pypi/idna@3.7",
3030+
"pkg:pypi/idna@9.9.9",
3031+
]
3032+
self.vulnerability1.save()
3033+
3034+
self.client.login(username=self.super_user.username, password="secret")
3035+
response = self.client.get(self.package1.details_url)
3036+
3037+
# A known package is linked directly.
3038+
self.assertContains(response, fixing_package.get_absolute_url())
3039+
# An unknown package offers an "Add Package" link instead.
3040+
self.assertContains(response, "idna@9.9.9")
3041+
self.assertContains(response, "package_url=pkg:pypi/idna@9.9.9")
3042+
30263043
def test_vulnerablecode_get_plain_purls(self):
30273044
purls = get_plain_purls(packages=[])
30283045
self.assertEqual([], purls)

component_catalog/views.py

Lines changed: 9 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
import json
1010
from collections import Counter
11-
from operator import itemgetter
1211
from urllib.parse import quote_plus
1312

1413
from django.apps import apps
@@ -261,7 +260,8 @@ def tab_vulnerabilities(self):
261260

262261
label = (
263262
f"Vulnerabilities"
264-
f' <span class="badge badge-vulnerability">{len(vulnerabilities_qs)}</span>'
263+
f' <span class="badge bg-danger-subtle text-danger-emphasis">'
264+
f"{len(vulnerabilities_qs)}</span>"
265265
)
266266

267267
vulnerabilities = []
@@ -280,55 +280,27 @@ def tab_vulnerabilities(self):
280280
}
281281

282282
def get_fixed_packages_html(self, vulnerability, dataspace):
283-
if not vulnerability.fixed_packages:
283+
if not vulnerability.fixed_by_packages:
284284
return
285285

286-
fixed_packages_sorted = natsorted(vulnerability.fixed_packages, key=itemgetter("purl"))
286+
fixed_packages_sorted = natsorted(vulnerability.fixed_by_packages)
287287
add_package_url = reverse("component_catalog:package_add")
288-
vulnerability_icon = (
289-
'<span data-bs-toggle="tooltip" title="Vulnerabilities"'
290-
' data-boundary="viewport">'
291-
'<i class="fas fa-bug vulnerability mx-1"></i>'
292-
"</span>"
293-
)
294-
no_vulnerabilities_icon = (
295-
'<span class="fa-stack fa-small text-muted-light ms-1"'
296-
' data-bs-toggle="tooltip" title="No vulnerabilities found"'
297-
' data-boundary="viewport">'
298-
' <i class="fas fa-bug fa-stack-1x"></i>'
299-
' <i class="fas fa-ban fa-stack-2x"></i>'
300-
"</span>"
301-
)
302288

303289
fixed_packages_values = []
304-
for fixed_package in fixed_packages_sorted:
305-
purl = fixed_package.get("purl")
306-
is_vulnerable = fixed_package.get("is_vulnerable")
290+
for purl in fixed_packages_sorted:
307291
package_instances = Package.objects.scope(dataspace).for_package_url(purl)
308292

309293
for package in package_instances:
310-
absolute_url = package.get_absolute_url()
311-
display_value = package.get_html_link(href=absolute_url)
312-
if is_vulnerable:
313-
display_value += package.get_html_link(
314-
href=f"{absolute_url}#vulnerabilities",
315-
value=mark_safe(vulnerability_icon),
316-
)
317-
else:
318-
display_value += no_vulnerabilities_icon
294+
display_value = package.get_html_link(href=package.get_absolute_url())
319295
fixed_packages_values.append(display_value)
320296

321297
if not package_instances:
322-
display_value = purl.replace("pkg:", "")
323-
if is_vulnerable:
324-
display_value += vulnerability_icon
325-
else:
326-
display_value += no_vulnerabilities_icon
327298
# Warning: do not add spaces between HTML elements as this content
328299
# is displayed in a <pre>
329-
display_value += (
300+
display_value = (
301+
f"{purl.replace('pkg:', '')}"
330302
f'<a href="{add_package_url}?package_url={purl}"'
331-
f' target="_blank">'
303+
f' class="ms-1" target="_blank">'
332304
f'<span data-bs-toggle="tooltip" title="Add Package"'
333305
f' data-boundary="viewport">'
334306
f'<i class="fas fa-plus-circle"></i>'

dejacode/static/css/dejacode_bootstrap.css

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -417,8 +417,8 @@ table.vulnerabilities-table .column-summary {
417417
width: 240px;
418418
}
419419
#tab_vulnerabilities .column-affected_packages {
420-
min-width: 300px;
421-
width: 300px;
420+
min-width: 310px;
421+
width: 310px;
422422
}
423423
#tab_vulnerabilities .column-triage_action {
424424
min-width: 165px;
@@ -603,12 +603,6 @@ table.purldb-table .column-license_expression {
603603
.vulnerability {
604604
color: #dc3545;
605605
}
606-
.badge-vulnerability {
607-
color: #fff;
608-
background-color: #dc3545;
609-
vertical-align: middle;
610-
}
611-
612606
#vulnerability-analysis-form fieldset legend {
613607
font-size: 1rem;
614608
}

dje/copier.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,8 @@
6767
"default_assignee",
6868
"affected_by_vulnerabilities",
6969
"risk_score",
70+
"next_non_vulnerable_version",
71+
"latest_non_vulnerable_version",
7072
]
7173

7274

dje/tests/testfiles/test_dataset_cc_only.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,9 @@
315315
"api_data_url": "",
316316
"datasource_id": "",
317317
"file_references": [],
318-
"parties": []
318+
"parties": [],
319+
"next_non_vulnerable_version": "",
320+
"latest_non_vulnerable_version": ""
319321
}
320322
},
321323
{

0 commit comments

Comments
 (0)