Skip to content

Commit 8c51b34

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

6 files changed

Lines changed: 280 additions & 5 deletions

File tree

vulnerabilities/api_v3.py

Lines changed: 102 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from django.db.models import Max
1616
from django.db.models import OuterRef
1717
from django.db.models import Prefetch
18+
from django.db.models import Q
1819
from django_filters import rest_framework as filters
1920
from drf_spectacular.utils import extend_schema
2021
from packageurl import PackageURL
@@ -33,7 +34,9 @@
3334
from vulnerabilities.models import Group
3435
from vulnerabilities.models import GroupedAdvisory
3536
from vulnerabilities.models import ImpactedPackageAffecting
37+
from vulnerabilities.models import PackageCommitPatch
3638
from vulnerabilities.models import PackageV2
39+
from vulnerabilities.models import Patch
3740
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
3841
from vulnerabilities.utils import TYPES_WITH_MULTIPLE_IMPORTERS
3942
from vulnerabilities.utils import get_advisories_from_groups
@@ -221,18 +224,24 @@ def get_affected_by_vulnerabilities(self, package):
221224
"""Return a dictionary with advisory as keys and their details, including fixed_by_packages."""
222225
advisories = self.context["advisory_map"].get(package.id, [])
223226
impact_map = self.context["impact_map"].get(package.id, {})
227+
introduced_patch_map = self.context.get("introduced_patch_map", {})
228+
fixed_patch_map = self.context.get("fixed_patch_map", {})
224229

225230
if advisories:
226231
result = []
227232

228233
for adv in advisories:
229234
fixed = impact_map.get(adv["avid"])
235+
introduced_patches = introduced_patch_map.get((package.id, adv["avid"]), [])
236+
fixed_patches = fixed_patch_map.get((package.id, adv["avid"]), [])
230237
adv.pop("avid", None)
231238

232239
result.append(
233240
{
234241
**adv,
235242
"fixed_by_packages": fixed,
243+
"introduced_in_patch": introduced_patches,
244+
"fixed_in_patch": fixed_patches,
236245
}
237246
)
238247

@@ -266,7 +275,8 @@ def get_affected_by_vulnerabilities(self, package):
266275
impact = impact_by_avid.get(advisory.avid)
267276
if not impact:
268277
continue
269-
278+
introduced_patches = introduced_patch_map.get((package.id, advisory.avid), [])
279+
fixed_patches = fixed_patch_map.get((package.id, advisory.avid), [])
270280
result.append(
271281
{
272282
"advisory_id": advisory.advisory_id.split("/")[-1],
@@ -276,9 +286,10 @@ def get_affected_by_vulnerabilities(self, package):
276286
"exploitability": advisory.exploitability,
277287
"risk_score": advisory.risk_score,
278288
"fixed_by_packages": [pkg.purl for pkg in impact.fixed_by_packages.all()],
289+
"introduced_in_patch": introduced_patches,
290+
"fixed_in_patch": fixed_patches,
279291
}
280292
)
281-
282293
return result
283294

284295
if not advisories:
@@ -386,6 +397,48 @@ def get_latest_non_vulnerable_version(self, package):
386397
return latest_non_vulnerable.version
387398

388399

400+
class PackageCommitPatchSerializer(serializers.ModelSerializer):
401+
introduced_in_advisories = serializers.SerializerMethodField()
402+
fixed_in_advisories = serializers.SerializerMethodField()
403+
404+
class Meta:
405+
model = PackageCommitPatch
406+
fields = [
407+
"commit_hash",
408+
"vcs_url",
409+
"commit_url",
410+
"patch_url",
411+
"introduced_in_advisories",
412+
"fixed_in_advisories",
413+
]
414+
415+
def get_introduced_in_advisories(self, obj):
416+
impacts = obj.introduced_in_impacts.all()
417+
return self.serialize_impacts(impacts)
418+
419+
def get_fixed_in_advisories(self, obj):
420+
impacts = obj.fixed_in_impacts.all()
421+
return self.serialize_impacts(impacts)
422+
423+
@staticmethod
424+
def serialize_impacts(impacts):
425+
unique_pairs = set()
426+
for impact in impacts:
427+
unique_pairs.add((impact.base_purl, impact.advisory.avid))
428+
return [{"purl": base_purl, "avid": avid} for base_purl, avid in unique_pairs]
429+
430+
431+
class PatchSerializer(serializers.ModelSerializer):
432+
in_advisories = serializers.SerializerMethodField()
433+
434+
class Meta:
435+
model = Patch
436+
fields = ["patch_url", "in_advisories"]
437+
438+
def get_in_advisories(self, obj):
439+
return [advisory.avid for advisory in obj.advisories.all()]
440+
441+
389442
class PackageV3ViewSet(viewsets.GenericViewSet):
390443
queryset = PackageV2.objects.all()
391444
serializer_class = PackageV3Serializer
@@ -456,6 +509,7 @@ def create(self, request, *args, **kwargs):
456509
affected_advisory_map = get_affected_advisories_bulk(page)
457510
fixing_advisory_map = get_fixing_advisories_bulk(page)
458511
impact_map = get_impacts_bulk(page)
512+
introduced_patch_map, fixed_patch_map = get_patches_bulk(page)
459513
serializer = self.get_serializer(
460514
page,
461515
many=True,
@@ -464,6 +518,8 @@ def create(self, request, *args, **kwargs):
464518
"advisory_map": affected_advisory_map,
465519
"impact_map": impact_map,
466520
"fixing_advisory_map": fixing_advisory_map,
521+
"introduced_patch_map": introduced_patch_map,
522+
"fixed_patch_map": fixed_patch_map,
467523
},
468524
)
469525
return self.get_paginated_response(serializer.data)
@@ -673,6 +729,50 @@ def get_impacts_bulk(packages):
673729
return impact_map
674730

675731

732+
def get_patches_bulk(packages):
733+
"""
734+
Returns a tuple of two dicts:
735+
- introduced_map: (package_id, advisory_avid) -> list of introduced patch dicts
736+
- fixed_map: (package_id, advisory_avid) -> list of fixed patch dicts
737+
Each patch dict contains 'commit_hash' and 'vcs_url'.
738+
Uses ImpactedPackageAffecting to link packages to impacts, then collects patches
739+
from introduced_by_package_commit_patches and fixed_by_package_commit_patches.
740+
"""
741+
package_ids = [p.id for p in packages]
742+
if not package_ids:
743+
return {}, {}
744+
745+
ipa_qs = (
746+
ImpactedPackageAffecting.objects.filter(package_id__in=package_ids)
747+
.select_related("impacted_package__advisory")
748+
.prefetch_related(
749+
"impacted_package__introduced_by_package_commit_patches",
750+
"impacted_package__fixed_by_package_commit_patches",
751+
)
752+
)
753+
754+
introduced_map = defaultdict(list)
755+
fixed_map = defaultdict(list)
756+
757+
for ipa in ipa_qs:
758+
pkg_id = ipa.package_id
759+
impact = ipa.impacted_package
760+
avid = impact.advisory.avid
761+
key = (pkg_id, avid)
762+
763+
for patch in impact.introduced_by_package_commit_patches.all():
764+
patch_data = {"commit_hash": patch.commit_hash, "vcs_url": patch.vcs_url}
765+
if patch_data not in introduced_map[key]:
766+
introduced_map[key].append(patch_data)
767+
768+
for patch in impact.fixed_by_package_commit_patches.all():
769+
patch_data = {"commit_hash": patch.commit_hash, "vcs_url": patch.vcs_url}
770+
if patch_data not in fixed_map[key]:
771+
fixed_map[key].append(patch_data)
772+
773+
return introduced_map, fixed_map
774+
775+
676776
def get_fixing_advisories_bulk(packages):
677777
package_ids = [p.id for p in packages]
678778

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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ def test_packages_post_without_details(self):
6666
def test_packages_post_with_details(self):
6767
url = reverse("package-v3-list")
6868

69-
with self.assertNumQueries(31):
69+
with self.assertNumQueries(34):
7070
response = self.client.post(
7171
url,
7272
data={

0 commit comments

Comments
 (0)