Skip to content

Commit e5aa2a0

Browse files
committed
Add API support for PackageCommitPatch
Add a test Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 8b90a51 commit e5aa2a0

6 files changed

Lines changed: 289 additions & 5 deletions

File tree

vulnerabilities/api_v3.py

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,18 +221,24 @@ def get_affected_by_vulnerabilities(self, package):
221221
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
222222
advisories = self.context["advisory_map"].get(package.id, [])
223223
impact_map = self.context["impact_map"].get(package.id, {})
224+
introduced_patch_map = self.context.get("introduced_patch_map", {})
225+
fixed_patch_map = self.context.get("fixed_patch_map", {})
224226

225227
if advisories:
226228
result = []
227229

228230
for adv in advisories:
229231
fixed = impact_map.get(adv["avid"])
232+
introduced_patches = introduced_patch_map.get((package.id, adv["avid"]), [])
233+
fixed_patches = fixed_patch_map.get((package.id, adv["avid"]), [])
230234
adv.pop("avid", None)
231235

232236
result.append(
233237
{
234238
**adv,
235239
"fixed_by_packages": fixed,
240+
"introduced_by_patch": introduced_patches,
241+
"fixed_by_patch": fixed_patches,
236242
}
237243
)
238244

@@ -266,7 +272,8 @@ def get_affected_by_vulnerabilities(self, package):
266272
impact = impact_by_avid.get(advisory.avid)
267273
if not impact:
268274
continue
269-
275+
introduced_patches = introduced_patch_map.get((package.id, advisory.avid), [])
276+
fixed_patches = fixed_patch_map.get((package.id, advisory.avid), [])
270277
result.append(
271278
{
272279
"advisory_id": advisory.advisory_id.split("/")[-1],
@@ -276,9 +283,10 @@ def get_affected_by_vulnerabilities(self, package):
276283
"exploitability": advisory.exploitability,
277284
"risk_score": advisory.risk_score,
278285
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
286+
"introduced_by_patch": introduced_patches,
287+
"fixed_by_patch": fixed_patches,
279288
}
280289
)
281-
282290
return result
283291

284292
if not advisories:
@@ -456,6 +464,7 @@ def create(self, request, *args, **kwargs):
456464
affected_advisory_map = get_affected_advisories_bulk(page)
457465
fixing_advisory_map = get_fixing_advisories_bulk(page)
458466
impact_map = get_impacts_bulk(page)
467+
introduced_patch_map, fixed_patch_map = get_patches_bulk(page)
459468
serializer = self.get_serializer(
460469
page,
461470
many=True,
@@ -464,6 +473,8 @@ def create(self, request, *args, **kwargs):
464473
"advisory_map": affected_advisory_map,
465474
"impact_map": impact_map,
466475
"fixing_advisory_map": fixing_advisory_map,
476+
"introduced_patch_map": introduced_patch_map,
477+
"fixed_patch_map": fixed_patch_map,
467478
},
468479
)
469480
return self.get_paginated_response(serializer.data)
@@ -673,6 +684,48 @@ def get_impacts_bulk(packages):
673684
return impact_map
674685

675686

687+
def get_patches_bulk(packages):
688+
"""
689+
Returns a tuple of two dicts:
690+
introduced_map: (package_id, advisory_avid) -> list of introduced package_commit_patches dicts
691+
fixed_map: (package_id, advisory_avid) -> list of fixed package_commit_patches dicts
692+
Each package_commit_patches dict contains 'commit_hash' and 'vcs_url'
693+
"""
694+
package_ids = [p.id for p in packages]
695+
if not package_ids:
696+
return {}, {}
697+
698+
impacted_packages_qs = (
699+
ImpactedPackageAffecting.objects.filter(package_id__in=package_ids)
700+
.select_related("impacted_package__advisory")
701+
.prefetch_related(
702+
"impacted_package__introduced_by_package_commit_patches",
703+
"impacted_package__fixed_by_package_commit_patches",
704+
)
705+
)
706+
707+
introduced_patches = defaultdict(list)
708+
fixed_patches = defaultdict(list)
709+
710+
for impacted_pkg_qs in impacted_packages_qs:
711+
pkg_id = impacted_pkg_qs.package_id
712+
impact = impacted_pkg_qs.impacted_package
713+
avid = impact.advisory.avid
714+
key = (pkg_id, avid)
715+
716+
for patch in impact.introduced_by_package_commit_patches.all():
717+
patch_data = {"commit_hash": patch.commit_hash, "vcs_url": patch.vcs_url}
718+
if patch_data not in introduced_patches[key]:
719+
introduced_patches[key].append(patch_data)
720+
721+
for patch in impact.fixed_by_package_commit_patches.all():
722+
patch_data = {"commit_hash": patch.commit_hash, "vcs_url": patch.vcs_url}
723+
if patch_data not in fixed_patches[key]:
724+
fixed_patches[key].append(patch_data)
725+
726+
return introduced_patches, fixed_patches
727+
728+
676729
def get_fixing_advisories_bulk(packages):
677730
package_ids = [p.id for p in packages]
678731

vulnerabilities/templates/advisory_detail.html

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,16 @@
8080
</a>
8181
</li>
8282
{% endif %}
83-
83+
84+
<li data-tab="patch-url">
85+
<a>
86+
<span>
87+
{% with pcp_length=package_commit_patches|length %}
88+
Patches: ({{ advisory.patches.count|add:pcp_length }})
89+
{% endwith %}
90+
</span>
91+
</a>
92+
</li>
8493
<!-- <li data-tab="history">
8594
<a>
8695
<span>
@@ -184,6 +193,18 @@
184193
</a>
185194
</td>
186195
</tr>
196+
<tr>
197+
<td class="two-col-left"
198+
data-tooltip="Risk expressed as a number ranging from 0 to 10. It is calculated by multiplying
199+
the weighted severity and exploitability values, capped at a maximum of 10.
200+
"
201+
>Introduced and Fixed Package Commit Patches</td>
202+
<td class="two-col-right wrap-strings">
203+
<a href="/advisories/commits/{{ advisory.avid }}">
204+
Package Commit Patches Details
205+
</a>
206+
</td>
207+
</tr>
187208
</tbody>
188209
</table>
189210
<div class="has-text-weight-bold tab-nested-div ml-1 mb-1 mt-6">
@@ -436,7 +457,6 @@
436457
</tr>
437458
{% endfor %}
438459
</div>
439-
440460

441461
<div class="tab-div content" data-content="epss">
442462
{% if epss_data %}
@@ -503,6 +523,28 @@
503523
{% endif %}
504524
</div>
505525

526+
<div class="tab-div content" data-content="patch-url">
527+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
528+
<thead>
529+
<tr>
530+
<th style="width: 250px;"> Patch URL </th>
531+
</tr>
532+
</thead>
533+
{% for patch in patches %}
534+
<tr>
535+
<td class="wrap-strings"><a href="{{ patch.patch_url }}" target="_blank">{{ patch.patch_url }}<i
536+
class="fa fa-external-link fa_link_custom"></i></a></td>
537+
</tr>
538+
{% empty %}
539+
<tr>
540+
<td colspan="2">
541+
There are no known patches.
542+
</td>
543+
</tr>
544+
{% endfor %}
545+
</table>
546+
</div>
547+
506548
<div class="tab-div content" data-content="severities-vectors">
507549
{% for severity_vector in severity_vectors %}
508550
{% if severity_vector.vector.version == '2.0' %}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
{% extends "base.html" %}
2+
{% load humanize %}
3+
{% load widget_tweaks %}
4+
{% load static %}
5+
{% load show_cvss %}
6+
{% load url_filters %}
7+
8+
{% block title %}
9+
VulnerableCode Advisory Package Commit Patch Details - {{ advisoryv2.advisory_id }}
10+
{% endblock %}
11+
12+
{% block content %}
13+
14+
{% if advisoryv2 %}
15+
<section class="section pt-0">
16+
<div class="details-container">
17+
<article class="panel is-info panel-header-only">
18+
<div class="panel-heading py-2 is-size-6">
19+
Introduce and Fixing Package Commit Patch details for Advisory:
20+
<span class="tag is-white custom">
21+
{{ advisoryv2.advisory_id }}
22+
</span>
23+
</div>
24+
</article>
25+
26+
<div id="tab-content">
27+
<table class="table vcio-table width-100-pct mt-2">
28+
<thead>
29+
<tr>
30+
<th style="width: 50%;">Introduced in</th>
31+
<th>Fixed by</th>
32+
</tr>
33+
</thead>
34+
<tbody>
35+
{% for impact in advisoryv2.impacted_packages.all %}
36+
{% for pkg_commit_patch in impact.introduced_by_package_commit_patches.all %}
37+
<tr>
38+
<td>
39+
<a href="{{ pkg_commit_patch.vcs_url }}" target="_self">
40+
{{ pkg_commit_patch.base_purl }}@{{ pkg_commit_patch.commit_hash }}
41+
</a>
42+
</td>
43+
<td></td>
44+
</tr>
45+
{% endfor %}
46+
47+
{% for pkg_commit_patch in impact.fixed_by_package_commit_patches.all %}
48+
<tr>
49+
<td></td>
50+
<td>
51+
<a href="{{ pkg_commit_patch.vcs_url }}" target="_self">
52+
{{ impact.base_purl }}@{{ pkg_commit_patch.commit_hash }}
53+
</a>
54+
</td>
55+
</tr>
56+
{% endfor %}
57+
58+
{% empty %}
59+
<tr>
60+
<td colspan="2">
61+
This vulnerability is not known to affect any package commits.
62+
</td>
63+
</tr>
64+
{% endfor %}
65+
</tbody>
66+
</table>
67+
</div>
68+
69+
</div>
70+
</section>
71+
{% endif %}
72+
73+
<script src="{% static 'js/main.js' %}" crossorigin="anonymous"></script>
74+
75+
{% endblock %}

vulnerabilities/tests/test_api_v3.py

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,20 @@
77
# See https://aboutcode.org for more information about nexB OSS projects.
88
#
99

10+
from django.test import TestCase
1011
from django.urls import reverse
1112
from packageurl import PackageURL
1213
from rest_framework import status
1314
from rest_framework.test import APIClient
1415
from rest_framework.test import APITestCase
1516
from univers.version_range import PypiVersionRange
1617

18+
from vulnerabilities.api_v3 import get_patches_bulk
1719
from vulnerabilities.importer import AdvisoryDataV2
1820
from vulnerabilities.models import AdvisoryV2
21+
from vulnerabilities.models import ImpactedPackage
22+
from vulnerabilities.models import ImpactedPackageAffecting
23+
from vulnerabilities.models import PackageCommitPatch
1924
from vulnerabilities.models import PackageV2
2025
from vulnerabilities.pipes.advisory import insert_advisory_v2
2126
from vulnerabilities.tests.pipelines import TestLogger
@@ -66,7 +71,7 @@ def test_packages_post_without_details(self):
6671
def test_packages_post_with_details(self):
6772
url = reverse("package-v3-list")
6873

69-
with self.assertNumQueries(31):
74+
with self.assertNumQueries(34):
7075
response = self.client.post(
7176
url,
7277
data={
@@ -244,3 +249,54 @@ def test_get_all_vulnerable_purls(self):
244249
results = response.data["results"]
245250
self.assertEqual(len(results), 100)
246251
self.assertIn("next", response.data)
252+
253+
254+
class PatchesBulkTests(TestCase):
255+
def setUp(self):
256+
self.advisory = AdvisoryV2.objects.create(
257+
avid="AVID-123",
258+
advisory_id="importer1/AVID-123",
259+
datasource_id="importer1",
260+
unique_content_id="c524de2f",
261+
url="https://github.com/aboutcode-org/vulnerablecode",
262+
)
263+
self.package = PackageV2.objects.from_purl(purl="pkg:pypi/sample@1.0.0")
264+
self.impact = ImpactedPackage.objects.create(advisory=self.advisory)
265+
ImpactedPackageAffecting.objects.create(package=self.package, impacted_package=self.impact)
266+
267+
self.intro_patch = PackageCommitPatch.objects.create(
268+
commit_hash="98e516011d6e096e25247b82fc5f196bbeecff10",
269+
vcs_url="https://github.com/aboutcode-org/vulnerablecode",
270+
)
271+
self.fixed_patch = PackageCommitPatch.objects.create(
272+
commit_hash="06580c7f99c6fde7bcf18e30bdcc61f081430957",
273+
vcs_url="https://github.com/aboutcode-org/vulnerablecode",
274+
)
275+
276+
self.impact.introduced_by_package_commit_patches.add(self.intro_patch)
277+
self.impact.fixed_by_package_commit_patches.add(self.fixed_patch)
278+
279+
def test_get_patches_bulk_logic(self):
280+
assert get_patches_bulk([]) == ({}, {})
281+
introduced_map, fixed_map = get_patches_bulk([self.package])
282+
expected_key = (self.package.id, self.advisory.avid)
283+
284+
assert introduced_map[expected_key] == [
285+
{
286+
"commit_hash": "98e516011d6e096e25247b82fc5f196bbeecff10",
287+
"vcs_url": "https://github.com/aboutcode-org/vulnerablecode",
288+
}
289+
]
290+
assert fixed_map[expected_key] == [
291+
{
292+
"commit_hash": "06580c7f99c6fde7bcf18e30bdcc61f081430957",
293+
"vcs_url": "https://github.com/aboutcode-org/vulnerablecode",
294+
}
295+
]
296+
297+
self.impact.introduced_by_package_commit_patches.clear()
298+
self.impact.fixed_by_package_commit_patches.clear()
299+
300+
intro_empty, fixed_empty = get_patches_bulk([self.package])
301+
assert intro_empty == {}
302+
assert fixed_empty == {}

0 commit comments

Comments
 (0)