Skip to content

Commit 65506e3

Browse files
committed
Add API support for Patch/PackageCommitPatch
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 65d7a58 commit 65506e3

File tree

7 files changed

+278
-3
lines changed

7 files changed

+278
-3
lines changed

vulnerabilities/api_v2.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010

1111
from django.db.models import Prefetch
12+
from django.db.models import Q
1213
from django_filters import rest_framework as filters
1314
from drf_spectacular.utils import OpenApiParameter
1415
from drf_spectacular.utils import extend_schema
@@ -33,14 +34,17 @@
3334
from vulnerabilities.models import CodeFixV2
3435
from vulnerabilities.models import ImpactedPackage
3536
from vulnerabilities.models import Package
37+
from vulnerabilities.models import PackageCommitPatch
3638
from vulnerabilities.models import PackageV2
39+
from vulnerabilities.models import Patch
3740
from vulnerabilities.models import PipelineRun
3841
from vulnerabilities.models import PipelineSchedule
3942
from vulnerabilities.models import Vulnerability
4043
from vulnerabilities.models import VulnerabilityReference
4144
from vulnerabilities.models import VulnerabilitySeverity
4245
from vulnerabilities.models import Weakness
4346
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
47+
from vulnerabilities.utils import generate_patch_url
4448
from vulnerabilities.utils import group_advisories_by_content
4549

4650

@@ -333,6 +337,48 @@ def get_fixing_vulnerabilities(self, obj):
333337
return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()]
334338

335339

340+
class PackageCommitPatchSerializer(serializers.ModelSerializer):
341+
introduced_in_advisories = serializers.SerializerMethodField()
342+
fixed_in_advisories = serializers.SerializerMethodField()
343+
344+
class Meta:
345+
model = PackageCommitPatch
346+
fields = [
347+
"id",
348+
"commit_hash",
349+
"vcs_url",
350+
"patch_url",
351+
"introduced_in_advisories",
352+
"fixed_in_advisories",
353+
]
354+
355+
def get_introduced_in_advisories(self, obj):
356+
impacts = obj.introduced_in_impacts.all()
357+
return self.serialize_impacts(impacts)
358+
359+
def get_fixed_in_advisories(self, obj):
360+
impacts = obj.fixed_in_impacts.all()
361+
return self.serialize_impacts(impacts)
362+
363+
@staticmethod
364+
def serialize_impacts(impacts):
365+
unique_pairs = set()
366+
for impact in impacts:
367+
unique_pairs.add((impact.base_purl, impact.advisory.avid))
368+
return [{"package": base_purl, "avid": avid} for base_purl, avid in unique_pairs]
369+
370+
371+
class PatchSerializer(serializers.ModelSerializer):
372+
in_advisories = serializers.SerializerMethodField()
373+
374+
class Meta:
375+
model = Patch
376+
fields = ["id", "patch_url", "in_advisories"]
377+
378+
def get_in_advisories(self, obj):
379+
return [{"avid": advisory.avid} for advisory in obj.advisories.all()]
380+
381+
336382
class PackageV3Serializer(serializers.ModelSerializer):
337383
purl = serializers.CharField(source="package_url")
338384
risk_score = serializers.FloatField(read_only=True)
@@ -889,6 +935,65 @@ def get_queryset(self):
889935
return queryset
890936

891937

938+
class PackageCommitPatchViewSet(viewsets.ReadOnlyModelViewSet):
939+
"""
940+
API endpoint that allows viewing PackageCommitPatch entries.
941+
"""
942+
943+
serializer_class = PackageCommitPatchSerializer
944+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
945+
946+
def get_queryset(self):
947+
queryset = PackageCommitPatch.objects.prefetch_related(
948+
"introduced_in_impacts__advisory", "fixed_in_impacts__advisory"
949+
)
950+
951+
pk = self.request.query_params.get("id")
952+
if pk:
953+
queryset = queryset.filter(id=pk)
954+
955+
advisory_id = self.request.query_params.get("advisory_id")
956+
if advisory_id:
957+
queryset = queryset.filter(
958+
Q(introduced_in_impacts__advisory__avid=advisory_id)
959+
| Q(fixed_in_impacts__advisory__avid=advisory_id)
960+
).distinct()
961+
962+
purl = self.request.query_params.get("purl")
963+
if purl:
964+
queryset = queryset.filter(
965+
Q(introduced_in_impacts__base_purl__icontains=purl)
966+
| Q(fixed_in_impacts__base_purl__icontains=purl)
967+
).distinct()
968+
969+
return queryset
970+
971+
972+
class PatchViewSet(viewsets.ReadOnlyModelViewSet):
973+
"""
974+
API endpoint that allows viewing PackageCommitPatch entries.
975+
"""
976+
977+
serializer_class = PatchSerializer
978+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
979+
980+
def get_queryset(self):
981+
queryset = Patch.objects.all()
982+
983+
pk = self.request.query_params.get("id")
984+
if pk:
985+
queryset = queryset.filter(id=pk)
986+
987+
advisory_id = self.request.query_params.get("advisory_id")
988+
if advisory_id:
989+
queryset = queryset.filter(advisory__advisory_id=advisory_id).distinct()
990+
991+
purl = self.request.query_params.get("purl")
992+
if purl:
993+
queryset = queryset.filter(package__package_url__icontains=purl).distinct()
994+
return queryset
995+
996+
892997
class CodeFixV2ViewSet(viewsets.ReadOnlyModelViewSet):
893998
"""
894999
API endpoint that allows viewing CodeFix entries.

vulnerabilities/models.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
from vulnerabilities.severity_systems import EPSS
7171
from vulnerabilities.severity_systems import SCORING_SYSTEMS
7272
from vulnerabilities.utils import compute_patch_checksum
73+
from vulnerabilities.utils import generate_commit_url
74+
from vulnerabilities.utils import generate_patch_url
7375
from vulnerabilities.utils import normalize_list
7476
from vulnerabilities.utils import normalize_purl
7577
from vulnerabilities.utils import normalize_text
@@ -2833,6 +2835,20 @@ class PackageCommitPatch(models.Model):
28332835
patch_text = models.TextField(blank=True, null=True)
28342836
patch_checksum = models.CharField(max_length=128, blank=True, null=True)
28352837

2838+
@property
2839+
def commit_url(self):
2840+
"""
2841+
Generates Commit URL using the VCS URL and Commit Hash.
2842+
"""
2843+
return generate_commit_url(self.vcs_url, self.commit_hash)
2844+
2845+
@property
2846+
def patch_url(self):
2847+
"""
2848+
Generates Patch URL using the VCS URL and Commit Hash.
2849+
"""
2850+
return generate_patch_url(self.vcs_url, self.commit_hash)
2851+
28362852
def save(self, *args, **kwargs):
28372853
if self.patch_text:
28382854
self.patch_checksum = compute_patch_checksum(self.patch_text)

vulnerabilities/templates/advisory_detail.html

Lines changed: 32 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>
@@ -413,7 +422,6 @@
413422
</tr>
414423
{% endfor %}
415424
</div>
416-
417425

418426
<div class="tab-div content" data-content="epss">
419427
{% if epss_data %}
@@ -480,6 +488,28 @@
480488
{% endif %}
481489
</div>
482490

491+
<div class="tab-div content" data-content="patch-url">
492+
<table class="table is-bordered is-striped is-narrow is-hoverable is-fullwidth">
493+
<thead>
494+
<tr>
495+
<th style="width: 250px;"> Patch URL </th>
496+
</tr>
497+
</thead>
498+
{% for patch in patches %}
499+
<tr>
500+
<td class="wrap-strings"><a href="{{ patch.patch_url }}" target="_blank">{{ patch.patch_url }}<i
501+
class="fa fa-external-link fa_link_custom"></i></a></td>
502+
</tr>
503+
{% empty %}
504+
<tr>
505+
<td colspan="2">
506+
There are no known patches.
507+
</td>
508+
</tr>
509+
{% endfor %}
510+
</table>
511+
</div>
512+
483513
<div class="tab-div content" data-content="severities-vectors">
484514
{% for severity_vector in severity_vectors %}
485515
{% if severity_vector.vector.version == '2.0' %}

vulnerabilities/templates/advisory_package_details.html

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,67 @@
6060
</div>
6161
</div>
6262
</section>
63+
<section class="section pt-0">
64+
<div class="details-container">
65+
<article class="panel is-info panel-header-only">
66+
<div class="panel-heading py-2 is-size-6">
67+
Vulnerable and Fixing Package Commit Patch details for Advisory:
68+
<span class="tag is-white custom">
69+
{{ advisoryv2.advisory_id }}
70+
</span>
71+
</div>
72+
</article>
73+
74+
<div id="tab-content">
75+
<table class="table vcio-table width-100-pct mt-2">
76+
<thead>
77+
<tr>
78+
<th style="width: 50%;">Introduced in</th>
79+
<th>Fixed by</th>
80+
</tr>
81+
</thead>
82+
<tbody>
83+
{% for impact in advisoryv2.impacted_packages.all %}
84+
85+
{% for pkg_commit_patch in impact.introduced_by_package_commit_patches.all %}
86+
<tr>
87+
<td>
88+
<a href="{{ pkg_commit_patch.patch_url }}" target="_self">
89+
{{ pkg_commit_patch.vcs_url }}@{{ pkg_commit_patch.commit_hash }}
90+
</a>
91+
<br />
92+
<a href="{{ pkg_commit_patch.patch_url }}" target="_self" class="text-muted">{{ pkg_commit_patch.patch_url }}</a>
93+
</td>
94+
<td></td>
95+
</tr>
96+
{% endfor %}
97+
98+
{% for pkg_commit_patch in impact.fixed_by_package_commit_patches.all %}
99+
<tr>
100+
<td></td>
101+
<td>
102+
<a href="{{ pkg_commit_patch.commit_url }}" target="_self">
103+
{{ impact.base_purl }}@{{ pkg_commit_patch.commit_hash }}
104+
</a>
105+
<br />
106+
<a href="{{ pkg_commit_patch.patch_url }}" target="_self" class="text-muted">{{ pkg_commit_patch.patch_url }}</a>
107+
</td>
108+
</tr>
109+
{% endfor %}
110+
111+
{% empty %}
112+
<tr>
113+
<td colspan="2" class="text-center">
114+
This vulnerability is not known to affect any package commits.
115+
</td>
116+
</tr>
117+
{% endfor %}
118+
</tbody>
119+
</table>
120+
</div>
121+
122+
</div>
123+
</section>
63124
{% endif %}
64125

65126
<script src="{% static 'js/main.js' %}" crossorigin="anonymous"></script>

vulnerabilities/utils.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from cwe2.database import InvalidCWEError
3636
from packageurl import PackageURL
3737
from packageurl.contrib.django.utils import without_empty_values
38+
from packageurl.contrib.purl2url import purl2url
39+
from packageurl.contrib.url2purl import url2purl
3840
from univers.version_range import RANGE_CLASS_BY_SCHEMES
3941
from univers.version_range import AlpineLinuxVersionRange
4042
from univers.version_range import NginxVersionRange
@@ -867,3 +869,51 @@ def group_advisories_by_content(advisories):
867869
entry["secondary"].add(advisory)
868870

869871
return grouped
872+
873+
874+
def generate_patch_url(vcs_url, commit_hash):
875+
"""
876+
Generate patch URL from VCS URL and commit hash.
877+
"""
878+
if not vcs_url or not commit_hash:
879+
return None
880+
881+
vcs_url = vcs_url.rstrip("/")
882+
883+
if vcs_url.startswith("https://github.com"):
884+
return f"{vcs_url}/commit/{commit_hash}.patch"
885+
elif vcs_url.startswith("https://gitlab.com"):
886+
return f"{vcs_url}/-/commit/{commit_hash}.patch"
887+
elif vcs_url.startswith("https://codeberg.org"):
888+
return f"{vcs_url}/-/commit/{commit_hash}.patch"
889+
elif vcs_url.startswith("https://android.googlesource.com"):
890+
return f"{vcs_url}/+/{commit_hash}%5E%21?format=TEXT"
891+
elif vcs_url.startswith("https://bitbucket.org"):
892+
return f"{vcs_url}/-/commit/{commit_hash}/raw"
893+
elif vcs_url.startswith("https://git.kernel.org"):
894+
return f"{vcs_url}/patch/?id={commit_hash}"
895+
return
896+
897+
898+
def generate_commit_url(vcs_url, commit_hash):
899+
"""
900+
Generate commit URL from VCS URL and commit hash.
901+
"""
902+
if not vcs_url or not commit_hash:
903+
return None
904+
905+
vcs_url = vcs_url.rstrip("/")
906+
907+
if vcs_url.startswith("https://github.com"):
908+
return f"{vcs_url}/commit/{commit_hash}"
909+
elif vcs_url.startswith("https://gitlab.com"):
910+
return f"{vcs_url}/-/commit/{commit_hash}"
911+
elif vcs_url.startswith("https://codeberg.org"):
912+
return f"{vcs_url}/-/commit/{commit_hash}"
913+
elif vcs_url.startswith("https://android.googlesource.com"):
914+
return f"{vcs_url}/+/{commit_hash}"
915+
elif vcs_url.startswith("https://bitbucket.org"):
916+
return f"{vcs_url}/-/commit/{commit_hash}"
917+
elif vcs_url.startswith("https://git.kernel.org"):
918+
return f"{vcs_url}/patch/?id={commit_hash}"
919+
return

0 commit comments

Comments
 (0)