1515from django .db .models import Max
1616from django .db .models import OuterRef
1717from django .db .models import Prefetch
18+ from django .db .models import Q
1819from django_filters import rest_framework as filters
1920from drf_spectacular .utils import extend_schema
2021from packageurl import PackageURL
3334from vulnerabilities .models import Group
3435from vulnerabilities .models import GroupedAdvisory
3536from vulnerabilities .models import ImpactedPackageAffecting
37+ from vulnerabilities .models import PackageCommitPatch
3638from vulnerabilities .models import PackageV2
39+ from vulnerabilities .models import Patch
3740from vulnerabilities .throttling import PermissionBasedUserRateThrottle
3841from vulnerabilities .utils import TYPES_WITH_MULTIPLE_IMPORTERS
3942from 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+
389442class 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+
676776def get_fixing_advisories_bulk (packages ):
677777 package_ids = [p .id for p in packages ]
678778
0 commit comments