From d1b139fedd6a2896b19e297add71290428d821f6 Mon Sep 17 00:00:00 2001 From: ziad hany Date: Sat, 21 Feb 2026 02:35:05 +0200 Subject: [PATCH] Add API support for Patch/PackageCommitPatch Signed-off-by: ziad hany --- vulnerabilities/api_v2.py | 105 ++++++++++++++++++ vulnerabilities/models.py | 16 +++ .../templates/advisory_detail.html | 34 +++++- .../templates/advisory_package_details.html | 61 ++++++++++ vulnerabilities/utils.py | 50 +++++++++ vulnerabilities/views.py | 18 ++- vulnerablecode/urls.py | 7 ++ 7 files changed, 288 insertions(+), 3 deletions(-) diff --git a/vulnerabilities/api_v2.py b/vulnerabilities/api_v2.py index 74975b819..92051d87c 100644 --- a/vulnerabilities/api_v2.py +++ b/vulnerabilities/api_v2.py @@ -9,6 +9,7 @@ from django.db.models import Prefetch +from django.db.models import Q from django_filters import rest_framework as filters from drf_spectacular.utils import OpenApiParameter from drf_spectacular.utils import extend_schema @@ -33,7 +34,9 @@ from vulnerabilities.models import CodeFixV2 from vulnerabilities.models import ImpactedPackage from vulnerabilities.models import Package +from vulnerabilities.models import PackageCommitPatch from vulnerabilities.models import PackageV2 +from vulnerabilities.models import Patch from vulnerabilities.models import PipelineRun from vulnerabilities.models import PipelineSchedule from vulnerabilities.models import Vulnerability @@ -41,6 +44,7 @@ from vulnerabilities.models import VulnerabilitySeverity from vulnerabilities.models import Weakness from vulnerabilities.throttling import PermissionBasedUserRateThrottle +from vulnerabilities.utils import generate_patch_url from vulnerabilities.utils import group_advisories_by_content @@ -333,6 +337,48 @@ def get_fixing_vulnerabilities(self, obj): return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()] +class PackageCommitPatchSerializer(serializers.ModelSerializer): + introduced_in_advisories = serializers.SerializerMethodField() + fixed_in_advisories = serializers.SerializerMethodField() + + class Meta: + model = PackageCommitPatch + fields = [ + "id", + "commit_hash", + "vcs_url", + "patch_url", + "introduced_in_advisories", + "fixed_in_advisories", + ] + + def get_introduced_in_advisories(self, obj): + impacts = obj.introduced_in_impacts.all() + return self.serialize_impacts(impacts) + + def get_fixed_in_advisories(self, obj): + impacts = obj.fixed_in_impacts.all() + return self.serialize_impacts(impacts) + + @staticmethod + def serialize_impacts(impacts): + unique_pairs = set() + for impact in impacts: + unique_pairs.add((impact.base_purl, impact.advisory.avid)) + return [{"package": base_purl, "avid": avid} for base_purl, avid in unique_pairs] + + +class PatchSerializer(serializers.ModelSerializer): + in_advisories = serializers.SerializerMethodField() + + class Meta: + model = Patch + fields = ["id", "patch_url", "in_advisories"] + + def get_in_advisories(self, obj): + return [{"avid": advisory.avid} for advisory in obj.advisories.all()] + + class PackageV3Serializer(serializers.ModelSerializer): purl = serializers.CharField(source="package_url") risk_score = serializers.FloatField(read_only=True) @@ -889,6 +935,65 @@ def get_queryset(self): return queryset +class PackageCommitPatchViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that allows viewing PackageCommitPatch entries. + """ + + serializer_class = PackageCommitPatchSerializer + throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle] + + def get_queryset(self): + queryset = PackageCommitPatch.objects.prefetch_related( + "introduced_in_impacts__advisory", "fixed_in_impacts__advisory" + ) + + pk = self.request.query_params.get("id") + if pk: + queryset = queryset.filter(id=pk) + + advisory_id = self.request.query_params.get("advisory_id") + if advisory_id: + queryset = queryset.filter( + Q(introduced_in_impacts__advisory__avid=advisory_id) + | Q(fixed_in_impacts__advisory__avid=advisory_id) + ).distinct() + + purl = self.request.query_params.get("purl") + if purl: + queryset = queryset.filter( + Q(introduced_in_impacts__base_purl__icontains=purl) + | Q(fixed_in_impacts__base_purl__icontains=purl) + ).distinct() + + return queryset + + +class PatchViewSet(viewsets.ReadOnlyModelViewSet): + """ + API endpoint that allows viewing PackageCommitPatch entries. + """ + + serializer_class = PatchSerializer + throttle_classes = [AnonRateThrottle, PermissionBasedUserRateThrottle] + + def get_queryset(self): + queryset = Patch.objects.all() + + pk = self.request.query_params.get("id") + if pk: + queryset = queryset.filter(id=pk) + + advisory_id = self.request.query_params.get("advisory_id") + if advisory_id: + queryset = queryset.filter(advisory__advisory_id=advisory_id).distinct() + + purl = self.request.query_params.get("purl") + if purl: + queryset = queryset.filter(package__package_url__icontains=purl).distinct() + return queryset + + class CodeFixV2ViewSet(viewsets.ReadOnlyModelViewSet): """ API endpoint that allows viewing CodeFix entries. diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 1981a2861..2ed5d7ca0 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -70,6 +70,8 @@ from vulnerabilities.severity_systems import EPSS from vulnerabilities.severity_systems import SCORING_SYSTEMS from vulnerabilities.utils import compute_patch_checksum +from vulnerabilities.utils import generate_commit_url +from vulnerabilities.utils import generate_patch_url from vulnerabilities.utils import normalize_list from vulnerabilities.utils import normalize_purl from vulnerabilities.utils import normalize_text @@ -2833,6 +2835,20 @@ class PackageCommitPatch(models.Model): patch_text = models.TextField(blank=True, null=True) patch_checksum = models.CharField(max_length=128, blank=True, null=True) + @property + def commit_url(self): + """ + Generates Commit URL using the VCS URL and Commit Hash. + """ + return generate_commit_url(self.vcs_url, self.commit_hash) + + @property + def patch_url(self): + """ + Generates Patch URL using the VCS URL and Commit Hash. + """ + return generate_patch_url(self.vcs_url, self.commit_hash) + def save(self, *args, **kwargs): if self.patch_text: self.patch_checksum = compute_patch_checksum(self.patch_text) diff --git a/vulnerabilities/templates/advisory_detail.html b/vulnerabilities/templates/advisory_detail.html index 595412df4..227bd9859 100644 --- a/vulnerabilities/templates/advisory_detail.html +++ b/vulnerabilities/templates/advisory_detail.html @@ -80,7 +80,16 @@ {% endif %} - + +
  • + + + {% with pcp_length=package_commit_patches|length %} + Patches: ({{ advisory.patches.count|add:pcp_length }}) + {% endwith %} + + +