Skip to content

Commit e458518

Browse files
committed
Add all packages endpoint functionality in V2
Signed-off-by: Tushar Goel <[email protected]>
1 parent 2ada438 commit e458518

File tree

2 files changed

+242
-5
lines changed

2 files changed

+242
-5
lines changed

vulnerabilities/api_v2.py

Lines changed: 63 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@
88
#
99

1010

11+
from drf_spectacular.utils import OpenApiParameter
12+
from drf_spectacular.utils import extend_schema
13+
from drf_spectacular.utils import extend_schema_view
14+
from packageurl import PackageURL
1115
from rest_framework import serializers
16+
from rest_framework import status
1217
from rest_framework import viewsets
18+
from rest_framework.decorators import action
1319
from rest_framework.response import Response
1420
from rest_framework.reverse import reverse
1521

@@ -18,8 +24,7 @@
1824
from vulnerabilities.models import VulnerabilityReference
1925
from vulnerabilities.models import VulnerabilitySeverity
2026
from vulnerabilities.models import Weakness
21-
from drf_spectacular.utils import extend_schema_view, extend_schema, OpenApiParameter
22-
from rest_framework.decorators import action
27+
2328

2429
class WeaknessV2Serializer(serializers.ModelSerializer):
2530
cwe_id = serializers.CharField()
@@ -90,6 +95,7 @@ def get_url(self, obj):
9095
request=request,
9196
)
9297

98+
9399
@extend_schema_view(
94100
list=extend_schema(
95101
parameters=[
@@ -197,6 +203,13 @@ class PackageBulkSearchRequestSerializer(PackageurlListSerializer):
197203
plain_purl = serializers.BooleanField(required=False, default=False)
198204

199205

206+
class LookupRequestSerializer(serializers.Serializer):
207+
purl = serializers.CharField(
208+
required=True,
209+
help_text="PackageURL strings in canonical form.",
210+
)
211+
212+
200213
class PackageV2ViewSet(viewsets.ReadOnlyModelViewSet):
201214
queryset = Package.objects.all()
202215
serializer_class = PackageV2Serializer
@@ -233,7 +246,7 @@ def list(self, request, *args, **kwargs):
233246
serializer = self.get_serializer(queryset, many=True)
234247
data = serializer.data
235248
return Response({"packages": data})
236-
249+
237250
@extend_schema(
238251
request=PackageurlListSerializer,
239252
responses={200: PackageV2Serializer(many=True)},
@@ -269,7 +282,6 @@ def bulk_lookup(self, request):
269282
).data
270283
)
271284

272-
273285
@extend_schema(
274286
request=PackageBulkSearchRequestSerializer,
275287
responses={200: PackageV2Serializer(many=True)},
@@ -333,8 +345,54 @@ def bulk_search(self, request):
333345
query = Package.objects.filter(package_url__in=purls).distinct().with_is_vulnerable()
334346

335347
if not purl_only:
336-
return Response(PackageV2Serializer(query, many=True, context={"request": request}).data)
348+
return Response(
349+
PackageV2Serializer(query, many=True, context={"request": request}).data
350+
)
337351

338352
vulnerable_purls = query.vulnerable().only("package_url")
339353
vulnerable_purls = [str(package.package_url) for package in vulnerable_purls]
340354
return Response(data=vulnerable_purls)
355+
356+
@action(detail=False, methods=["get"])
357+
def all(self, request):
358+
"""
359+
Return a list of Package URLs of vulnerable packages.
360+
"""
361+
vulnerable_purls = (
362+
Package.objects.vulnerable()
363+
.only("package_url")
364+
.order_by("package_url")
365+
.distinct()
366+
.values_list("package_url", flat=True)
367+
)
368+
return Response(vulnerable_purls)
369+
370+
@extend_schema(
371+
request=LookupRequestSerializer,
372+
responses={200: PackageV2Serializer(many=True)},
373+
)
374+
@action(
375+
detail=False,
376+
methods=["post"],
377+
serializer_class=LookupRequestSerializer,
378+
filter_backends=[],
379+
pagination_class=None,
380+
)
381+
def lookup(self, request):
382+
"""
383+
Return the response for exact PackageURL requested for.
384+
"""
385+
serializer = self.serializer_class(data=request.data)
386+
if not serializer.is_valid():
387+
return Response(
388+
status=status.HTTP_400_BAD_REQUEST,
389+
data={
390+
"error": serializer.errors,
391+
"message": "A 'purl' is required.",
392+
},
393+
)
394+
validated_data = serializer.validated_data
395+
purl = validated_data.get("purl")
396+
397+
qs = self.get_queryset().for_purls([purl]).with_is_vulnerable()
398+
return Response(PackageV2Serializer(qs, many=True, context={"request": request}).data)

vulnerabilities/tests/test_api_v2.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,3 +305,182 @@ def test_get_fixing_vulnerabilities(self):
305305
serializer = PackageV2Serializer()
306306
vulnerabilities = serializer.get_fixing_vulnerabilities(package)
307307
self.assertEqual(vulnerabilities, ["VCID-5678"])
308+
309+
def test_bulk_lookup_with_valid_purls(self):
310+
"""
311+
Test bulk lookup with valid PURLs.
312+
"""
313+
url = reverse("package-v2-bulk-lookup")
314+
data = {"purls": ["pkg:pypi/[email protected]", "pkg:npm/[email protected]"]}
315+
response = self.client.post(url, data, format="json")
316+
self.assertEqual(response.status_code, status.HTTP_200_OK)
317+
self.assertEqual(len(response.data), 2)
318+
# Verify that the returned data matches the packages
319+
purls = [package["purl"] for package in response.data]
320+
self.assertIn("pkg:pypi/[email protected]", purls)
321+
self.assertIn("pkg:npm/[email protected]", purls)
322+
323+
def test_bulk_lookup_with_invalid_purls(self):
324+
"""
325+
Test bulk lookup with invalid PURLs.
326+
"""
327+
url = reverse("package-v2-bulk-lookup")
328+
data = {"purls": ["pkg:pypi/[email protected]", "pkg:npm/[email protected]"]}
329+
response = self.client.post(url, data, format="json")
330+
self.assertEqual(response.status_code, status.HTTP_200_OK)
331+
# Since the packages don't exist, the response should be empty
332+
self.assertEqual(len(response.data), 0)
333+
334+
def test_bulk_lookup_with_empty_purls(self):
335+
"""
336+
Test bulk lookup with empty purls list.
337+
Should return 400 Bad Request.
338+
"""
339+
url = reverse("package-v2-bulk-lookup")
340+
data = {"purls": []}
341+
response = self.client.post(url, data, format="json")
342+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
343+
self.assertIn("error", response.data)
344+
self.assertIn("message", response.data)
345+
self.assertEqual(response.data["message"], "A non-empty 'purls' list of PURLs is required.")
346+
347+
def test_bulk_search_with_valid_purls(self):
348+
"""
349+
Test bulk search with valid PURLs.
350+
"""
351+
url = reverse("package-v2-bulk-search")
352+
data = {"purls": ["pkg:pypi/[email protected]", "pkg:npm/[email protected]"]}
353+
response = self.client.post(url, data, format="json")
354+
self.assertEqual(response.status_code, status.HTTP_200_OK)
355+
self.assertEqual(len(response.data), 2)
356+
purls = [package["purl"] for package in response.data]
357+
self.assertIn("pkg:pypi/[email protected]", purls)
358+
self.assertIn("pkg:npm/[email protected]", purls)
359+
360+
def test_bulk_search_with_purl_only_true(self):
361+
"""
362+
Test bulk search with purl_only set to True.
363+
Should return only the PURLs of vulnerable packages.
364+
"""
365+
url = reverse("package-v2-bulk-search")
366+
data = {"purls": ["pkg:pypi/[email protected]", "pkg:npm/[email protected]"], "purl_only": True}
367+
response = self.client.post(url, data, format="json")
368+
self.assertEqual(response.status_code, status.HTTP_200_OK)
369+
# Since purl_only=True, response should be a list of PURLs
370+
self.assertIsInstance(response.data, list)
371+
# Only vulnerable packages should be included
372+
self.assertEqual(len(response.data), 1)
373+
self.assertEqual(response.data, ["pkg:pypi/[email protected]"])
374+
375+
def test_bulk_search_with_plain_purl_true(self):
376+
"""
377+
Test bulk search with plain_purl set to True.
378+
"""
379+
url = reverse("package-v2-bulk-search")
380+
data = {"purls": ["pkg:pypi/[email protected]", "pkg:pypi/[email protected]"], "plain_purl": True}
381+
response = self.client.post(url, data, format="json")
382+
self.assertEqual(response.status_code, status.HTTP_200_OK)
383+
# Since plain_purl=True, packages with the same name and version are grouped
384+
self.assertEqual(len(response.data), 1)
385+
purls = [package["purl"] for package in response.data]
386+
self.assertIn("pkg:pypi/[email protected]", purls[0] or "pkg:pypi/[email protected]" in purls[0])
387+
388+
def test_bulk_search_with_purl_only_and_plain_purl_true(self):
389+
"""
390+
Test bulk search with purl_only and plain_purl both set to True.
391+
Should return only the plain PURLs of vulnerable packages.
392+
"""
393+
url = reverse("package-v2-bulk-search")
394+
data = {
395+
"purls": ["pkg:pypi/[email protected]", "pkg:pypi/[email protected]"],
396+
"purl_only": True,
397+
"plain_purl": True,
398+
}
399+
response = self.client.post(url, data, format="json")
400+
self.assertEqual(response.status_code, status.HTTP_200_OK)
401+
# Response should be a list of plain PURLs
402+
self.assertIsInstance(response.data, list)
403+
# Only one plain PURL should be returned for vulnerable packages
404+
self.assertEqual(len(response.data), 1)
405+
self.assertEqual(response.data, ["pkg:pypi/[email protected]"])
406+
407+
def test_bulk_search_with_invalid_purls(self):
408+
"""
409+
Test bulk search with invalid PURLs.
410+
"""
411+
url = reverse("package-v2-bulk-search")
412+
data = {"purls": ["pkg:pypi/[email protected]", "pkg:npm/[email protected]"]}
413+
response = self.client.post(url, data, format="json")
414+
self.assertEqual(response.status_code, status.HTTP_200_OK)
415+
self.assertEqual(len(response.data), 0)
416+
417+
def test_bulk_search_with_empty_purls(self):
418+
"""
419+
Test bulk search with empty purls list.
420+
Should return 400 Bad Request.
421+
"""
422+
url = reverse("package-v2-bulk-search")
423+
data = {"purls": []}
424+
response = self.client.post(url, data, format="json")
425+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
426+
self.assertIn("error", response.data)
427+
self.assertIn("message", response.data)
428+
self.assertEqual(response.data["message"], "A non-empty 'purls' list of PURLs is required.")
429+
430+
def test_all_vulnerable_packages(self):
431+
"""
432+
Test the 'all' endpoint that returns all vulnerable package URLs.
433+
"""
434+
url = reverse("package-v2-all")
435+
response = self.client.get(url, format="json")
436+
self.assertEqual(response.status_code, status.HTTP_200_OK)
437+
# Since package1 and package3 are vulnerable, they should be returned
438+
expected_purls = ["pkg:pypi/[email protected]"]
439+
self.assertEqual(sorted(response.data), sorted(expected_purls))
440+
441+
def test_lookup_with_valid_purl(self):
442+
"""
443+
Test the 'lookup' endpoint with a valid PURL.
444+
"""
445+
url = reverse("package-v2-lookup")
446+
data = {"purl": "pkg:pypi/[email protected]"}
447+
response = self.client.post(url, data, format="json")
448+
self.assertEqual(response.status_code, status.HTTP_200_OK)
449+
self.assertEqual(len(response.data), 1)
450+
self.assertEqual(response.data[0]["purl"], "pkg:pypi/[email protected]")
451+
self.assertEqual(response.data[0]["affected_by_vulnerabilities"], ["VCID-1234"])
452+
453+
def test_lookup_with_invalid_purl(self):
454+
"""
455+
Test the 'lookup' endpoint with a PURL that does not exist.
456+
Should return an empty list.
457+
"""
458+
url = reverse("package-v2-lookup")
459+
data = {"purl": "pkg:pypi/[email protected]"}
460+
response = self.client.post(url, data, format="json")
461+
self.assertEqual(response.status_code, status.HTTP_200_OK)
462+
# No packages should be returned
463+
self.assertEqual(len(response.data), 0)
464+
465+
def test_lookup_with_missing_purl(self):
466+
"""
467+
Test the 'lookup' endpoint without providing a 'purl'.
468+
Should return 400 Bad Request.
469+
"""
470+
url = reverse("package-v2-lookup")
471+
data = {}
472+
response = self.client.post(url, data, format="json")
473+
self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)
474+
self.assertIn("error", response.data)
475+
self.assertIn("message", response.data)
476+
self.assertEqual(response.data["message"], "A 'purl' is required.")
477+
478+
def test_lookup_with_invalid_purl_format(self):
479+
"""
480+
Test the 'lookup' endpoint with an invalid PURL format.
481+
Should return 400 Bad Request.
482+
"""
483+
url = reverse("package-v2-lookup")
484+
data = {"purl": "invalid_purl_format"}
485+
response = self.client.post(url, data, format="json")
486+
self.assertEqual(response.status_code, status.HTTP_200_OK)

0 commit comments

Comments
 (0)