Skip to content

Commit 58d606f

Browse files
authored
Merge branch 'main' into prepare_release_v34.3.0
2 parents efb4f57 + ae605f4 commit 58d606f

File tree

3 files changed

+559
-8
lines changed

3 files changed

+559
-8
lines changed

vulnerabilities/api_v2.py

Lines changed: 297 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,20 @@
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
1116
from rest_framework import serializers
17+
from rest_framework import status
1218
from rest_framework import viewsets
19+
from rest_framework.decorators import action
1320
from rest_framework.response import Response
1421
from rest_framework.reverse import reverse
1522

23+
from vulnerabilities.api import PackageFilterSet
24+
from vulnerabilities.api import VulnerabilitySeveritySerializer
1625
from vulnerabilities.models import Package
1726
from vulnerabilities.models import Vulnerability
1827
from 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+
)
93122
class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet):
94123
queryset = Vulnerability.objects.all()
95124
serializer_class = VulnerabilityV2Serializer
@@ -142,6 +171,7 @@ def list(self, request, *args, **kwargs):
142171

143172
class 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+
167226
class 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

Comments
 (0)