Skip to content

Commit 939055a

Browse files
authored
Add endpoint for purl lookup (#1359)
* Add endpoint for purl lookup Signed-off-by: Tushar Goel <[email protected]> * Add endpoint for bulk_lookup Signed-off-by: Tushar Goel <[email protected]> * Add tests for purl lookup Signed-off-by: Tushar Goel <[email protected]> --------- Signed-off-by: Tushar Goel <[email protected]>
1 parent 5092a79 commit 939055a

File tree

3 files changed

+134
-0
lines changed

3 files changed

+134
-0
lines changed

vulnerabilities/api.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,42 @@ def all(self, request):
347347
vulnerable_purls = [str(package.package_url) for package in vulnerable_packages]
348348
return Response(vulnerable_purls)
349349

350+
@action(detail=False, methods=["post"])
351+
def lookup(self, request):
352+
"""
353+
Return the response for exact PackageURL requested for.
354+
"""
355+
purl = request.data.get("purl")
356+
if not purl:
357+
return Response(
358+
status=400,
359+
data={"Error": "A 'purl' is required."},
360+
)
361+
return Response(
362+
PackageSerializer(
363+
Package.objects.for_purls([purl]), many=True, context={"request": request}
364+
).data
365+
)
366+
367+
@action(detail=False, methods=["post"])
368+
def bulk_lookup(self, request):
369+
"""
370+
Return the response for exact PackageURLs requested for.
371+
"""
372+
purls = request.data.get("purls") or []
373+
if not purls:
374+
return Response(
375+
status=400,
376+
data={"Error": "A non-empty 'purls' list of PURLs is required."},
377+
)
378+
return Response(
379+
PackageSerializer(
380+
Package.objects.for_purls(purls),
381+
many=True,
382+
context={"request": request},
383+
).data
384+
)
385+
350386

351387
class VulnerabilityFilterSet(filters.FilterSet):
352388
class Meta:

vulnerabilities/models.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,9 @@ def for_cve(self, cve):
546546
"""
547547
return self.filter(vulnerabilities__vulnerabilityreference__reference_id__exact=cve)
548548

549+
def for_purls(self, purls=[]):
550+
return Package.objects.filter(package_url__in=purls).distinct()
551+
549552

550553
def get_purl_query_lookups(purl):
551554
"""

vulnerabilities/tests/test_api.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -749,3 +749,98 @@ class TesBanUserAgent(TestCase):
749749
def test_ban_request_with_bytedance_user_agent(self):
750750
response = self.client.get(f"/api/packages", format="json", HTTP_USER_AGENT="bytedance")
751751
assert 404 == response.status_code
752+
753+
754+
class TestLookup(TestCase):
755+
def setUp(self):
756+
Package.objects.create(
757+
type="pypi", namespace="", name="microweber/microweber", version="1.2"
758+
)
759+
self.user = ApiUser.objects.create_api_user(username="[email protected]")
760+
self.auth = f"Token {self.user.auth_token.key}"
761+
self.csrf_client = APIClient(enforce_csrf_checks=True)
762+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
763+
764+
def test_lookup_endpoint_failure(self):
765+
request_body = {"purl": None}
766+
response = self.csrf_client.post(
767+
"/api/packages/lookup",
768+
data=json.dumps(request_body),
769+
content_type="application/json",
770+
).json()
771+
assert response == {"Error": "A 'purl' is required."}
772+
773+
def test_lookup_endpoint(self):
774+
request_body = {"purl": "pkg:pypi/microweber/[email protected]"}
775+
response = self.csrf_client.post(
776+
"/api/packages/lookup",
777+
data=json.dumps(request_body),
778+
content_type="application/json",
779+
).json()
780+
assert len(response) == 1
781+
assert response[0]["purl"] == "pkg:pypi/microweber/[email protected]"
782+
783+
def test_lookup_endpoint_1(self):
784+
Package.objects.create(
785+
type="pypi", namespace="microweber", name="microweber", version="1.2"
786+
)
787+
request_body = {"purl": "pkg:pypi/microweber/[email protected]"}
788+
response = self.csrf_client.post(
789+
"/api/packages/lookup",
790+
data=json.dumps(request_body),
791+
content_type="application/json",
792+
).json()
793+
assert len(response) == 2
794+
assert response[0]["purl"] == "pkg:pypi/microweber/[email protected]"
795+
assert response[0]["purl"] == response[1]["purl"]
796+
797+
def test_lookup_endpoint_with_one_purl_with_qualifier(self):
798+
Package.objects.create(
799+
type="pypi", namespace="microweber", name="microweber", version="1.2"
800+
)
801+
Package.objects.create(
802+
type="pypi",
803+
namespace="microweber",
804+
name="microweber",
805+
version="1.2",
806+
qualifiers={"foo": "bar"},
807+
)
808+
request_body = {"purl": "pkg:pypi/microweber/[email protected]"}
809+
response = self.csrf_client.post(
810+
"/api/packages/lookup",
811+
data=json.dumps(request_body),
812+
content_type="application/json",
813+
).json()
814+
assert len(response) == 2
815+
assert response[0]["purl"] == "pkg:pypi/microweber/[email protected]"
816+
assert response[0]["purl"] == response[1]["purl"]
817+
request_body = {"purl": "pkg:pypi/microweber/[email protected]?foo=bar"}
818+
response = self.csrf_client.post(
819+
"/api/packages/lookup",
820+
data=json.dumps(request_body),
821+
content_type="application/json",
822+
).json()
823+
assert len(response) == 1
824+
assert response[0]["purl"] == "pkg:pypi/microweber/[email protected]?foo=bar"
825+
request_body = {"purl": "pkg:pypi/microweber/[email protected]?foo=baz"}
826+
response = self.csrf_client.post(
827+
"/api/packages/lookup",
828+
data=json.dumps(request_body),
829+
content_type="application/json",
830+
).json()
831+
assert response == []
832+
833+
def test_bulk_lookup_endpoint(self):
834+
request_body = {
835+
"purls": [
836+
"pkg:pypi/microweber/[email protected]?foo=bar",
837+
"pkg:pypi/microweber/[email protected]",
838+
"pkg:pypi/foo/[email protected]",
839+
],
840+
}
841+
response = self.csrf_client.post(
842+
"/api/packages/bulk_lookup",
843+
data=json.dumps(request_body),
844+
content_type="application/json",
845+
).json()
846+
assert len(response) == 1

0 commit comments

Comments
 (0)