88#
99
1010
11+ from django_filters import rest_framework as filters
12+ from drf_spectacular .utils import OpenApiParameter
13+ from drf_spectacular .utils import extend_schema
14+ from drf_spectacular .utils import extend_schema_view
15+ from packageurl import PackageURL
1116from rest_framework import serializers
17+ from rest_framework import status
1218from rest_framework import viewsets
19+ from rest_framework .decorators import action
1320from rest_framework .response import Response
1421from rest_framework .reverse import reverse
1522
23+ from vulnerabilities .api import PackageFilterSet
24+ from vulnerabilities .api import VulnerabilitySeveritySerializer
1625from vulnerabilities .models import Package
1726from vulnerabilities .models import Vulnerability
1827from vulnerabilities .models import VulnerabilityReference
@@ -90,6 +99,26 @@ def get_url(self, obj):
9099 )
91100
92101
102+ @extend_schema_view (
103+ list = extend_schema (
104+ parameters = [
105+ OpenApiParameter (
106+ name = "vulnerability_id" ,
107+ description = "Filter by one or more vulnerability IDs" ,
108+ required = False ,
109+ type = {"type" : "array" , "items" : {"type" : "string" }},
110+ location = OpenApiParameter .QUERY ,
111+ ),
112+ OpenApiParameter (
113+ name = "alias" ,
114+ description = "Filter by alias (CVE or other unique identifier)" ,
115+ required = False ,
116+ type = {"type" : "array" , "items" : {"type" : "string" }},
117+ location = OpenApiParameter .QUERY ,
118+ ),
119+ ]
120+ )
121+ )
93122class VulnerabilityV2ViewSet (viewsets .ReadOnlyModelViewSet ):
94123 queryset = Vulnerability .objects .all ()
95124 serializer_class = VulnerabilityV2Serializer
@@ -142,6 +171,7 @@ def list(self, request, *args, **kwargs):
142171
143172class PackageV2Serializer (serializers .ModelSerializer ):
144173 purl = serializers .CharField (source = "package_url" )
174+ risk_score = serializers .FloatField (read_only = True )
145175 affected_by_vulnerabilities = serializers .SerializerMethodField ()
146176 fixing_vulnerabilities = serializers .SerializerMethodField ()
147177 next_non_vulnerable_version = serializers .CharField (read_only = True )
@@ -155,6 +185,7 @@ class Meta:
155185 "fixing_vulnerabilities" ,
156186 "next_non_vulnerable_version" ,
157187 "latest_non_vulnerable_version" ,
188+ "risk_score" ,
158189 ]
159190
160191 def get_affected_by_vulnerabilities (self , obj ):
@@ -164,9 +195,39 @@ def get_fixing_vulnerabilities(self, obj):
164195 return [vuln .vulnerability_id for vuln in obj .fixing_vulnerabilities .all ()]
165196
166197
198+ class PackageurlListSerializer (serializers .Serializer ):
199+ purls = serializers .ListField (
200+ child = serializers .CharField (),
201+ allow_empty = False ,
202+ help_text = "List of PackageURL strings in canonical form." ,
203+ )
204+
205+
206+ class PackageBulkSearchRequestSerializer (PackageurlListSerializer ):
207+ purl_only = serializers .BooleanField (required = False , default = False )
208+ plain_purl = serializers .BooleanField (required = False , default = False )
209+
210+
211+ class LookupRequestSerializer (serializers .Serializer ):
212+ purl = serializers .CharField (
213+ required = True ,
214+ help_text = "PackageURL strings in canonical form." ,
215+ )
216+
217+
218+ class PackageV2FilterSet (filters .FilterSet ):
219+ affected_by_vulnerability = filters .CharFilter (
220+ field_name = "affected_by_vulnerabilities__vulnerability_id"
221+ )
222+ fixing_vulnerability = filters .CharFilter (field_name = "fixing_vulnerabilities__vulnerability_id" )
223+ purl = filters .CharFilter (field_name = "package_url" )
224+
225+
167226class PackageV2ViewSet (viewsets .ReadOnlyModelViewSet ):
168227 queryset = Package .objects .all ()
169228 serializer_class = PackageV2Serializer
229+ filter_backends = (filters .DjangoFilterBackend ,)
230+ filterset_class = PackageV2FilterSet
170231
171232 def get_queryset (self ):
172233 queryset = super ().get_queryset ()
@@ -188,15 +249,248 @@ def get_queryset(self):
188249
189250 def list (self , request , * args , ** kwargs ):
190251 queryset = self .get_queryset ()
252+
191253 # Apply pagination
192254 page = self .paginate_queryset (queryset )
193255 if page is not None :
256+ # Collect only vulnerabilities for packages in the current page
257+ vulnerabilities = set ()
258+ for package in page :
259+ vulnerabilities .update (package .affected_by_vulnerabilities .all ())
260+ vulnerabilities .update (package .fixing_vulnerabilities .all ())
261+
262+ # Serialize the vulnerabilities with vulnerability_id as keys
263+ vulnerability_data = {
264+ vuln .vulnerability_id : VulnerabilityV2Serializer (vuln ).data
265+ for vuln in vulnerabilities
266+ }
267+
268+ # Serialize the current page of packages
194269 serializer = self .get_serializer (page , many = True )
195270 data = serializer .data
271+
196272 # Use 'self.get_paginated_response' to include pagination data
197- return self .get_paginated_response ({"packages" : data })
273+ return self .get_paginated_response (
274+ {"vulnerabilities" : vulnerability_data , "packages" : data }
275+ )
276+
277+ # If pagination is not applied, collect vulnerabilities for all packages
278+ vulnerabilities = set ()
279+ for package in queryset :
280+ vulnerabilities .update (package .affected_by_vulnerabilities .all ())
281+ vulnerabilities .update (package .fixing_vulnerabilities .all ())
198282
199- # If pagination is not applied
283+ vulnerability_data = {
284+ vuln .vulnerability_id : VulnerabilityV2Serializer (vuln ).data for vuln in vulnerabilities
285+ }
286+
287+ # Serialize all packages when pagination is not applied
200288 serializer = self .get_serializer (queryset , many = True )
201289 data = serializer .data
202- return Response ({"packages" : data })
290+ return Response ({"vulnerabilities" : vulnerability_data , "packages" : data })
291+
292+ @extend_schema (
293+ request = PackageurlListSerializer ,
294+ responses = {200 : PackageV2Serializer (many = True )},
295+ )
296+ @action (
297+ detail = False ,
298+ methods = ["post" ],
299+ serializer_class = PackageurlListSerializer ,
300+ filter_backends = [],
301+ pagination_class = None ,
302+ )
303+ def bulk_lookup (self , request ):
304+ """
305+ Return the response for exact PackageURLs requested for.
306+ """
307+ serializer = self .serializer_class (data = request .data )
308+ if not serializer .is_valid ():
309+ return Response (
310+ status = status .HTTP_400_BAD_REQUEST ,
311+ data = {
312+ "error" : serializer .errors ,
313+ "message" : "A non-empty 'purls' list of PURLs is required." ,
314+ },
315+ )
316+ validated_data = serializer .validated_data
317+ purls = validated_data .get ("purls" )
318+
319+ # Fetch packages matching the provided purls
320+ packages = Package .objects .for_purls (purls ).with_is_vulnerable ()
321+
322+ # Collect vulnerabilities associated with these packages
323+ vulnerabilities = set ()
324+ for package in packages :
325+ vulnerabilities .update (package .affected_by_vulnerabilities .all ())
326+ vulnerabilities .update (package .fixing_vulnerabilities .all ())
327+
328+ # Serialize vulnerabilities with vulnerability_id as keys
329+ vulnerability_data = {
330+ vuln .vulnerability_id : VulnerabilityV2Serializer (vuln ).data for vuln in vulnerabilities
331+ }
332+
333+ # Serialize packages
334+ package_data = PackageV2Serializer (
335+ packages ,
336+ many = True ,
337+ context = {"request" : request },
338+ ).data
339+
340+ return Response (
341+ {
342+ "vulnerabilities" : vulnerability_data ,
343+ "packages" : package_data ,
344+ }
345+ )
346+
347+ @extend_schema (
348+ request = PackageBulkSearchRequestSerializer ,
349+ responses = {200 : PackageV2Serializer (many = True )},
350+ )
351+ @action (
352+ detail = False ,
353+ methods = ["post" ],
354+ serializer_class = PackageBulkSearchRequestSerializer ,
355+ filter_backends = [],
356+ pagination_class = None ,
357+ )
358+ def bulk_search (self , request ):
359+ """
360+ Lookup for vulnerable packages using many Package URLs at once.
361+ """
362+ serializer = self .serializer_class (data = request .data )
363+ if not serializer .is_valid ():
364+ return Response (
365+ status = status .HTTP_400_BAD_REQUEST ,
366+ data = {
367+ "error" : serializer .errors ,
368+ "message" : "A non-empty 'purls' list of PURLs is required." ,
369+ },
370+ )
371+ validated_data = serializer .validated_data
372+ purls = validated_data .get ("purls" )
373+ purl_only = validated_data .get ("purl_only" , False )
374+ plain_purl = validated_data .get ("plain_purl" , False )
375+
376+ if plain_purl :
377+ purl_objects = [PackageURL .from_string (purl ) for purl in purls ]
378+ plain_purl_objects = [
379+ PackageURL (
380+ type = purl .type ,
381+ namespace = purl .namespace ,
382+ name = purl .name ,
383+ version = purl .version ,
384+ )
385+ for purl in purl_objects
386+ ]
387+ plain_purls = [str (purl ) for purl in plain_purl_objects ]
388+
389+ query = (
390+ Package .objects .filter (plain_package_url__in = plain_purls )
391+ .order_by ("plain_package_url" )
392+ .distinct ("plain_package_url" )
393+ .with_is_vulnerable ()
394+ )
395+
396+ packages = query
397+
398+ # Collect vulnerabilities associated with these packages
399+ vulnerabilities = set ()
400+ for package in packages :
401+ vulnerabilities .update (package .affected_by_vulnerabilities .all ())
402+ vulnerabilities .update (package .fixing_vulnerabilities .all ())
403+
404+ vulnerability_data = {
405+ vuln .vulnerability_id : VulnerabilityV2Serializer (vuln ).data
406+ for vuln in vulnerabilities
407+ }
408+
409+ if not purl_only :
410+ package_data = PackageV2Serializer (
411+ packages , many = True , context = {"request" : request }
412+ ).data
413+ return Response (
414+ {
415+ "vulnerabilities" : vulnerability_data ,
416+ "packages" : package_data ,
417+ }
418+ )
419+
420+ # Using order by and distinct because there will be
421+ # many fully qualified purl for a single plain purl
422+ vulnerable_purls = query .vulnerable ().only ("plain_package_url" )
423+ vulnerable_purls = [str (package .plain_package_url ) for package in vulnerable_purls ]
424+ return Response (data = vulnerable_purls )
425+
426+ query = Package .objects .filter (package_url__in = purls ).distinct ().with_is_vulnerable ()
427+ packages = query
428+
429+ # Collect vulnerabilities associated with these packages
430+ vulnerabilities = set ()
431+ for package in packages :
432+ vulnerabilities .update (package .affected_by_vulnerabilities .all ())
433+ vulnerabilities .update (package .fixing_vulnerabilities .all ())
434+
435+ vulnerability_data = {
436+ vuln .vulnerability_id : VulnerabilityV2Serializer (vuln ).data for vuln in vulnerabilities
437+ }
438+
439+ if not purl_only :
440+ package_data = PackageV2Serializer (
441+ packages , many = True , context = {"request" : request }
442+ ).data
443+ return Response (
444+ {
445+ "vulnerabilities" : vulnerability_data ,
446+ "packages" : package_data ,
447+ }
448+ )
449+
450+ vulnerable_purls = query .vulnerable ().only ("package_url" )
451+ vulnerable_purls = [str (package .package_url ) for package in vulnerable_purls ]
452+ return Response (data = vulnerable_purls )
453+
454+ @action (detail = False , methods = ["get" ])
455+ def all (self , request ):
456+ """
457+ Return a list of Package URLs of vulnerable packages.
458+ """
459+ vulnerable_purls = (
460+ Package .objects .vulnerable ()
461+ .only ("package_url" )
462+ .order_by ("package_url" )
463+ .distinct ()
464+ .values_list ("package_url" , flat = True )
465+ )
466+ return Response (vulnerable_purls )
467+
468+ @extend_schema (
469+ request = LookupRequestSerializer ,
470+ responses = {200 : PackageV2Serializer (many = True )},
471+ )
472+ @action (
473+ detail = False ,
474+ methods = ["post" ],
475+ serializer_class = LookupRequestSerializer ,
476+ filter_backends = [],
477+ pagination_class = None ,
478+ )
479+ def lookup (self , request ):
480+ """
481+ Return the response for exact PackageURL requested for.
482+ """
483+ serializer = self .serializer_class (data = request .data )
484+ if not serializer .is_valid ():
485+ return Response (
486+ status = status .HTTP_400_BAD_REQUEST ,
487+ data = {
488+ "error" : serializer .errors ,
489+ "message" : "A 'purl' is required." ,
490+ },
491+ )
492+ validated_data = serializer .validated_data
493+ purl = validated_data .get ("purl" )
494+
495+ qs = self .get_queryset ().for_purls ([purl ]).with_is_vulnerable ()
496+ return Response (PackageV2Serializer (qs , many = True , context = {"request" : request }).data )
0 commit comments