Skip to content

Commit 2ada438

Browse files
committed
Add bulk search in V2 API
Signed-off-by: Tushar Goel <[email protected]>
1 parent 8bca5cc commit 2ada438

File tree

1 file changed

+140
-2
lines changed

1 file changed

+140
-2
lines changed

vulnerabilities/api_v2.py

Lines changed: 140 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
from vulnerabilities.models import VulnerabilityReference
1919
from vulnerabilities.models import VulnerabilitySeverity
2020
from vulnerabilities.models import Weakness
21-
21+
from drf_spectacular.utils import extend_schema_view, extend_schema, OpenApiParameter
22+
from rest_framework.decorators import action
2223

2324
class WeaknessV2Serializer(serializers.ModelSerializer):
2425
cwe_id = serializers.CharField()
@@ -89,7 +90,26 @@ def get_url(self, obj):
8990
request=request,
9091
)
9192

92-
93+
@extend_schema_view(
94+
list=extend_schema(
95+
parameters=[
96+
OpenApiParameter(
97+
name="vulnerability_id",
98+
description="Filter by one or more vulnerability IDs",
99+
required=False,
100+
type={"type": "array", "items": {"type": "string"}},
101+
location=OpenApiParameter.QUERY,
102+
),
103+
OpenApiParameter(
104+
name="alias",
105+
description="Filter by alias (CVE or other unique identifier)",
106+
required=False,
107+
type={"type": "array", "items": {"type": "string"}},
108+
location=OpenApiParameter.QUERY,
109+
),
110+
]
111+
)
112+
)
93113
class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet):
94114
queryset = Vulnerability.objects.all()
95115
serializer_class = VulnerabilityV2Serializer
@@ -164,6 +184,19 @@ def get_fixing_vulnerabilities(self, obj):
164184
return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()]
165185

166186

187+
class PackageurlListSerializer(serializers.Serializer):
188+
purls = serializers.ListField(
189+
child=serializers.CharField(),
190+
allow_empty=False,
191+
help_text="List of PackageURL strings in canonical form.",
192+
)
193+
194+
195+
class PackageBulkSearchRequestSerializer(PackageurlListSerializer):
196+
purl_only = serializers.BooleanField(required=False, default=False)
197+
plain_purl = serializers.BooleanField(required=False, default=False)
198+
199+
167200
class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet):
168201
queryset = Package.objects.all()
169202
serializer_class = PackageV2Serializer
@@ -200,3 +233,108 @@ def list(self, request, *args, **kwargs):
200233
serializer = self.get_serializer(queryset, many=True)
201234
data = serializer.data
202235
return Response({"packages": data})
236+
237+
@extend_schema(
238+
request=PackageurlListSerializer,
239+
responses={200: PackageV2Serializer(many=True)},
240+
)
241+
@action(
242+
detail=False,
243+
methods=["post"],
244+
serializer_class=PackageurlListSerializer,
245+
filter_backends=[],
246+
pagination_class=None,
247+
)
248+
def bulk_lookup(self, request):
249+
"""
250+
Return the response for exact PackageURLs requested for.
251+
"""
252+
serializer = self.serializer_class(data=request.data)
253+
if not serializer.is_valid():
254+
return Response(
255+
status=status.HTTP_400_BAD_REQUEST,
256+
data={
257+
"error": serializer.errors,
258+
"message": "A non-empty 'purls' list of PURLs is required.",
259+
},
260+
)
261+
validated_data = serializer.validated_data
262+
purls = validated_data.get("purls")
263+
264+
return Response(
265+
PackageV2Serializer(
266+
Package.objects.for_purls(purls).with_is_vulnerable(),
267+
many=True,
268+
context={"request": request},
269+
).data
270+
)
271+
272+
273+
@extend_schema(
274+
request=PackageBulkSearchRequestSerializer,
275+
responses={200: PackageV2Serializer(many=True)},
276+
)
277+
@action(
278+
detail=False,
279+
methods=["post"],
280+
serializer_class=PackageBulkSearchRequestSerializer,
281+
filter_backends=[],
282+
pagination_class=None,
283+
)
284+
def bulk_search(self, request):
285+
"""
286+
Lookup for vulnerable packages using many Package URLs at once.
287+
"""
288+
serializer = self.serializer_class(data=request.data)
289+
if not serializer.is_valid():
290+
return Response(
291+
status=status.HTTP_400_BAD_REQUEST,
292+
data={
293+
"error": serializer.errors,
294+
"message": "A non-empty 'purls' list of PURLs is required.",
295+
},
296+
)
297+
validated_data = serializer.validated_data
298+
purls = validated_data.get("purls")
299+
purl_only = validated_data.get("purl_only", False)
300+
plain_purl = validated_data.get("plain_purl", False)
301+
302+
if plain_purl:
303+
purl_objects = [PackageURL.from_string(purl) for purl in purls]
304+
plain_purl_objects = [
305+
PackageURL(
306+
type=purl.type,
307+
namespace=purl.namespace,
308+
name=purl.name,
309+
version=purl.version,
310+
)
311+
for purl in purl_objects
312+
]
313+
plain_purls = [str(purl) for purl in plain_purl_objects]
314+
315+
query = (
316+
Package.objects.filter(plain_package_url__in=plain_purls)
317+
.order_by("plain_package_url")
318+
.distinct("plain_package_url")
319+
.with_is_vulnerable()
320+
)
321+
322+
if not purl_only:
323+
return Response(
324+
PackageV2Serializer(query, many=True, context={"request": request}).data
325+
)
326+
327+
# using order by and distinct because there will be
328+
# many fully qualified purl for a single plain purl
329+
vulnerable_purls = query.vulnerable().only("plain_package_url")
330+
vulnerable_purls = [str(package.plain_package_url) for package in vulnerable_purls]
331+
return Response(data=vulnerable_purls)
332+
333+
query = Package.objects.filter(package_url__in=purls).distinct().with_is_vulnerable()
334+
335+
if not purl_only:
336+
return Response(PackageV2Serializer(query, many=True, context={"request": request}).data)
337+
338+
vulnerable_purls = query.vulnerable().only("package_url")
339+
vulnerable_purls = [str(package.package_url) for package in vulnerable_purls]
340+
return Response(data=vulnerable_purls)

0 commit comments

Comments
 (0)