Skip to content

Commit b9d4b05

Browse files
committed
Add API support for Patch/PackageCommitPatch
Signed-off-by: ziad hany <ziadhany2016@gmail.com>
1 parent 94a9c8f commit b9d4b05

File tree

3 files changed

+187
-0
lines changed

3 files changed

+187
-0
lines changed

vulnerabilities/api_v2.py

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@
3333
from vulnerabilities.models import CodeFixV2
3434
from vulnerabilities.models import ImpactedPackage
3535
from vulnerabilities.models import Package
36+
from vulnerabilities.models import PackageCommitPatch
3637
from vulnerabilities.models import PackageV2
38+
from vulnerabilities.models import Patch
3739
from vulnerabilities.models import PipelineRun
3840
from vulnerabilities.models import PipelineSchedule
3941
from vulnerabilities.models import Vulnerability
4042
from vulnerabilities.models import VulnerabilityReference
4143
from vulnerabilities.models import VulnerabilitySeverity
4244
from vulnerabilities.models import Weakness
4345
from vulnerabilities.throttling import PermissionBasedUserRateThrottle
46+
from vulnerabilities.utils import get_patch_url
4447
from vulnerabilities.utils import group_advisories_by_content
4548

4649

@@ -333,20 +336,49 @@ def get_fixing_vulnerabilities(self, obj):
333336
return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()]
334337

335338

339+
class PackageCommitPatchSerializer(serializers.ModelSerializer):
340+
patch_url = serializers.SerializerMethodField()
341+
342+
class Meta:
343+
model = PackageCommitPatch
344+
fields = [
345+
"id",
346+
"commit_hash",
347+
"vcs_url",
348+
"patch_url",
349+
]
350+
351+
def get_patch_url(self, obj):
352+
return get_patch_url(obj.vcs_url, obj.commit_hash)
353+
354+
355+
class PatchSerializer(serializers.ModelSerializer):
356+
class Meta:
357+
model = Patch
358+
fields = [
359+
"id",
360+
"patch_url",
361+
]
362+
363+
336364
class PackageV3Serializer(serializers.ModelSerializer):
337365
purl = serializers.CharField(source="package_url")
338366
risk_score = serializers.FloatField(read_only=True)
339367
affected_by_vulnerabilities = serializers.SerializerMethodField()
340368
fixing_vulnerabilities = serializers.SerializerMethodField()
341369
next_non_vulnerable_version = serializers.SerializerMethodField()
342370
latest_non_vulnerable_version = serializers.SerializerMethodField()
371+
introduced_by_package_commit_patches = serializers.SerializerMethodField()
372+
fixed_by_package_commit_patches = serializers.SerializerMethodField()
343373

344374
class Meta:
345375
model = Package
346376
fields = [
347377
"purl",
348378
"affected_by_vulnerabilities",
349379
"fixing_vulnerabilities",
380+
"introduced_by_package_commit_patches",
381+
"fixed_by_package_commit_patches",
350382
"next_non_vulnerable_version",
351383
"latest_non_vulnerable_version",
352384
"risk_score",
@@ -425,6 +457,103 @@ def get_fixing_vulnerabilities(self, package):
425457

426458
return result
427459

460+
def get_introduced_by_package_commit_patches(self, package):
461+
# 1. Route through 'affected_in_impacts' to get to the ImpactedPackage model
462+
# and prefetch the commit patches to avoid N+1 queries.
463+
impacts = package.affected_in_impacts.select_related("advisory").prefetch_related(
464+
"introduced_by_package_commit_patches"
465+
)
466+
467+
avids = {impact.advisory.avid for impact in impacts if impact.advisory_id}
468+
if not avids:
469+
return []
470+
471+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
472+
advisory_by_avid = {adv.avid: adv for adv in latest_advisories}
473+
impact_by_avid = {}
474+
475+
advisories = []
476+
for impact in impacts:
477+
avid = impact.advisory.avid
478+
advisory = advisory_by_avid.get(avid)
479+
if not advisory:
480+
continue
481+
advisories.append(advisory)
482+
impact_by_avid[avid] = impact
483+
484+
grouped_advisories = group_advisories_by_content(advisories=advisories)
485+
486+
result = []
487+
# Minor cleanup: You can iterate over .values() directly instead of appending to a new list
488+
for advisory_group in grouped_advisories.values():
489+
primary_advisory = advisory_group["primary"]
490+
avid = primary_advisory.avid
491+
impact = impact_by_avid.get(avid)
492+
493+
if not impact:
494+
continue
495+
496+
# 2. Grab the actual commit patches from the impact
497+
patches = impact.introduced_by_package_commit_patches.all()
498+
if not patches:
499+
continue
500+
501+
result.append(
502+
{
503+
"advisory_id": primary_advisory.avid,
504+
"duplicate_advisory_ids": [adv.avid for adv in advisory_group["secondary"]],
505+
# 3. Output the commit data (assuming PackageCommitPatch has a to_dict() method)
506+
"commit_patches": [patch.to_dict() for patch in patches],
507+
}
508+
)
509+
510+
return result
511+
512+
def get_fixed_by_package_commit_patches(self, package):
513+
impacts = package.affected_in_impacts.select_related("advisory").prefetch_related(
514+
"fixed_by_package_commit_patches"
515+
)
516+
517+
avids = {impact.advisory.avid for impact in impacts if impact.advisory_id}
518+
if not avids:
519+
return []
520+
521+
latest_advisories = AdvisoryV2.objects.latest_for_avids(avids)
522+
advisory_by_avid = {adv.avid: adv for adv in latest_advisories}
523+
impact_by_avid = {}
524+
525+
advisories = []
526+
for impact in impacts:
527+
avid = impact.advisory.avid
528+
if advisory := advisory_by_avid.get(avid):
529+
advisories.append(advisory)
530+
impact_by_avid[avid] = impact
531+
532+
grouped_advisories = group_advisories_by_content(advisories=advisories)
533+
534+
result = []
535+
for advisory_group in grouped_advisories.values():
536+
primary_advisory = advisory_group["primary"]
537+
impact = impact_by_avid.get(primary_advisory.avid)
538+
539+
if not impact:
540+
continue
541+
542+
# Query the fixing patches instead
543+
patches = impact.fixed_by_package_commit_patches.all()
544+
if not patches:
545+
continue
546+
547+
result.append(
548+
{
549+
"advisory_id": primary_advisory.avid,
550+
"duplicate_advisory_ids": [adv.avid for adv in advisory_group["secondary"]],
551+
"commit_patches": [patch.to_dict() for patch in patches],
552+
}
553+
)
554+
555+
return result
556+
428557
def get_next_non_vulnerable_version(self, package):
429558
if next_non_vulnerable := package.get_non_vulnerable_versions()[0]:
430559
return next_non_vulnerable.version
@@ -889,6 +1018,40 @@ def get_queryset(self):
8891018
return queryset
8901019

8911020

1021+
class PackageCommitPatchViewSet(viewsets.ReadOnlyModelViewSet):
1022+
"""
1023+
API endpoint that allows viewing PackageCommitPatch entries.
1024+
"""
1025+
1026+
queryset = PackageCommitPatch.objects.all()
1027+
serializer_class = PackageCommitPatchSerializer
1028+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
1029+
1030+
def get_queryset(self):
1031+
queryset = PackageCommitPatch.objects.all()
1032+
pk = self.request.query_params.get("id")
1033+
if pk:
1034+
queryset = queryset.filter(id=pk)
1035+
return queryset
1036+
1037+
1038+
class PatchViewSet(viewsets.ReadOnlyModelViewSet):
1039+
"""
1040+
API endpoint that allows viewing PackageCommitPatch entries.
1041+
"""
1042+
1043+
queryset = Patch.objects.all()
1044+
serializer_class = PatchSerializer
1045+
throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle]
1046+
1047+
def get_queryset(self):
1048+
queryset = Patch.objects.all()
1049+
pk = self.request.query_params.get("id")
1050+
if pk:
1051+
queryset = queryset.filter(id=pk)
1052+
return queryset
1053+
1054+
8921055
class CodeFixV2ViewSet(viewsets.ReadOnlyModelViewSet):
8931056
"""
8941057
API endpoint that allows viewing CodeFix entries.

vulnerabilities/utils.py

Lines changed: 17 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
@@ -888,3 +890,18 @@ def group_advisories_by_content(advisories):
888890
entry["secondary"].add(advisory)
889891

890892
return grouped
893+
894+
895+
def get_patch_url(vcs_url, commit_hash):
896+
"""
897+
Generate patch URL from VCS URL and commit hash.
898+
"""
899+
if vcs_url.startswith("https://github.com"):
900+
return f"{vcs_url}/commit/{commit_hash}.patch"
901+
elif vcs_url.startswith("https://gitlab.com"):
902+
return f"{vcs_url}/-/commit/{commit_hash}.patch"
903+
elif vcs_url.startswith("https://bitbucket.org"):
904+
return f"{vcs_url}/-/commit/{commit_hash}/raw"
905+
elif vcs_url.startswith("https://git.kernel.org"):
906+
return f"{vcs_url}.git/patch/?id={commit_hash}"
907+
return

vulnerablecode/urls.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,10 @@
2222
from vulnerabilities.api import VulnerabilityViewSet
2323
from vulnerabilities.api_v2 import CodeFixV2ViewSet
2424
from vulnerabilities.api_v2 import CodeFixViewSet
25+
from vulnerabilities.api_v2 import PackageCommitPatchViewSet
2526
from vulnerabilities.api_v2 import PackageV2ViewSet
2627
from vulnerabilities.api_v2 import PackageV3ViewSet
28+
from vulnerabilities.api_v2 import PatchViewSet
2729
from vulnerabilities.api_v2 import PipelineScheduleV2ViewSet
2830
from vulnerabilities.api_v2 import VulnerabilityV2ViewSet
2931
from vulnerabilities.views import AdminLoginView
@@ -71,6 +73,11 @@ def __init__(self, *args, **kwargs):
7173

7274
api_v3_router.register("packages", PackageV3ViewSet, basename="package-v3")
7375

76+
api_v3_router.register(
77+
"package_commit_patches", PackageCommitPatchViewSet, basename="package_commit_patch"
78+
)
79+
api_v3_router.register("patches", PatchViewSet, basename="patches")
80+
7481
urlpatterns = [
7582
path("admin/login/", AdminLoginView.as_view(), name="admin-login"),
7683
path("api/v2/", include(api_v2_router.urls)),

0 commit comments

Comments
 (0)