Skip to content

Commit b8548b5

Browse files
committed
Allow querying /api/packages by Package URL
Signed-off-by: Haiko Schol <hs@haikoschol.com>
1 parent 0ce2512 commit b8548b5

2 files changed

Lines changed: 75 additions & 10 deletions

File tree

vulnerabilities/api.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
2323

2424
from rest_framework import serializers
25+
from rest_framework import status
2526
from rest_framework import viewsets
27+
from rest_framework.response import Response
28+
29+
from packageurl import PackageURL
2630

2731
from vulnerabilities.models import Package
2832
from vulnerabilities.models import PackageReference
@@ -82,3 +86,26 @@ class PackageViewSet(viewsets.ReadOnlyModelViewSet):
8286
queryset = Package.objects.all()
8387
serializer_class = PackageSerializer
8488
filter_fields = ('name', 'version')
89+
90+
def filter_queryset(self, qs):
91+
purl = self.request.query_params.get('package_url')
92+
if not purl:
93+
return super().filter_queryset(qs)
94+
95+
try:
96+
purl = PackageURL.from_string(purl)
97+
except ValueError as ve:
98+
raise serializers.ValidationError(
99+
detail={'error': f'"{purl}" is not a valid Package URL: {ve}'},
100+
)
101+
102+
# Remove "qualifiers" here because it is stored as one string in the model.
103+
# For example, a row in the database could have the "qualifiers" column
104+
# stored as "foo=bar&spam=eggs". If an API request contains a PURL with
105+
# "spam=eggs&foo=bar", the DB query would not include that row.
106+
attrs = {k: v for k, v in purl.to_dict().items() if v and k != 'qualifiers'}
107+
108+
# TODO
109+
# Since we are filtering on all the Package URL fields except "qualifiers",
110+
# we'll eventually need database indices on them.
111+
return self.queryset.filter(**attrs)

vulnerabilities/tests/test_api.py

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -47,26 +47,64 @@ def setUpTestData(cls):
4747
extract_data = debian.extract_vulnerabilities(test_data)
4848
debian_dump(extract_data)
4949

50-
def setUp(self):
51-
self.response = self.client.get('/api/packages/?name=mimetex', format='json').data
50+
Package.objects.create(
51+
name='mimetex',
52+
version='1.50-1.1',
53+
type='deb',
54+
namespace='ubuntu'
55+
)
5256

53-
def test_count(self):
54-
self.assertEqual(4, self.response['count'])
57+
def test_query_by_name(self):
58+
response = self.client.get('/api/packages/?name=mimetex', format='json').data
5559

56-
def test_name(self):
57-
first_result = self.response['results'][0]
60+
self.assertEqual(5, response['count'])
61+
62+
first_result = response['results'][0]
5863
self.assertEqual('mimetex', first_result['name'])
5964

60-
def test_version(self):
61-
versions = {r['version'] for r in self.response['results']}
65+
versions = {r['version'] for r in response['results']}
6266
self.assertIn('1.50-1.1', versions)
6367
self.assertIn('1.74-1', versions)
6468

65-
def test_packageurl(self):
66-
purls = {r['package_url'] for r in self.response['results']}
69+
purls = {r['package_url'] for r in response['results']}
6770
self.assertIn('pkg:deb/debian/mimetex@1.50-1.1?distro=jessie', purls)
6871
self.assertIn('pkg:deb/debian/mimetex@1.74-1?distro=jessie', purls)
6972

73+
def test_query_by_invalid_package_url(self):
74+
url = '/api/packages/?package_url=invalid_purl'
75+
response = self.client.get(url, format='json')
76+
77+
self.assertEqual(400, response.status_code)
78+
self.assertIn('error', response.data)
79+
error = response.data['error']
80+
self.assertIn('invalid_purl', error)
81+
82+
def test_query_by_package_url(self):
83+
url = '/api/packages/?package_url=pkg:deb/debian/mimetex@1.50-1.1?distro=jessie'
84+
response = self.client.get(url, format='json').data
85+
86+
self.assertEqual(2, response['count'])
87+
88+
first_result = response['results'][0]
89+
self.assertEqual('mimetex', first_result['name'])
90+
91+
versions = {r['version'] for r in response['results']}
92+
self.assertIn('1.50-1.1', versions)
93+
self.assertNotIn('1.74-1', versions)
94+
95+
def test_query_by_package_url_without_namespace(self):
96+
url = '/api/packages/?package_url=pkg:deb/mimetex@1.50-1.1'
97+
response = self.client.get(url, format='json').data
98+
99+
self.assertEqual(3, response['count'])
100+
101+
first_result = response['results'][0]
102+
self.assertEqual('mimetex', first_result['name'])
103+
104+
purls = {r['package_url'] for r in response['results']}
105+
self.assertIn('pkg:deb/debian/mimetex@1.50-1.1?distro=jessie', purls)
106+
self.assertIn('pkg:deb/ubuntu/mimetex@1.50-1.1', purls)
107+
70108

71109
class TestUbuntuResponse(TestCase):
72110
def test_ubuntu_response(self):

0 commit comments

Comments
 (0)