Skip to content

Commit deca049

Browse files
committed
Add bulk search in V2 API
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 8a68c97 commit deca049

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
@@ -19,7 +19,8 @@
1919
from vulnerabilities.models import VulnerabilityReference
2020
from vulnerabilities.models import VulnerabilitySeverity
2121
from vulnerabilities.models import Weakness
22-
22+
from drf_spectacular.utils import extend_schema_view, extend_schema, OpenApiParameter
23+
from rest_framework.decorators import action
2324

2425
class WeaknessV2Serializer(serializers.ModelSerializer):
2526
cwe_id = serializers.CharField()
@@ -80,7 +81,26 @@ def get_url(self, obj):
8081
request=request,
8182
)
8283

83-
84+
@extend_schema_view(
85+
list=extend_schema(
86+
parameters=[
87+
OpenApiParameter(
88+
name="vulnerability_id",
89+
description="Filter by one or more vulnerability IDs",
90+
required=False,
91+
type={"type": "array", "items": {"type": "string"}},
92+
location=OpenApiParameter.QUERY,
93+
),
94+
OpenApiParameter(
95+
name="alias",
96+
description="Filter by alias (CVE or other unique identifier)",
97+
required=False,
98+
type={"type": "array", "items": {"type": "string"}},
99+
location=OpenApiParameter.QUERY,
100+
),
101+
]
102+
)
103+
)
84104
class VulnerabilityV2ViewSet(viewsets.ReadOnlyModelViewSet):
85105
queryset = Vulnerability.objects.all()
86106
serializer_class = VulnerabilityV2Serializer
@@ -155,6 +175,19 @@ def get_fixing_vulnerabilities(self, obj):
155175
return [vuln.vulnerability_id for vuln in obj.fixing_vulnerabilities.all()]
156176

157177

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

0 commit comments

Comments
 (0)