Skip to content

Commit 49052d6

Browse files
authored
Merge pull request #784 from TG1999/api_fixed_packages
Add fixed packages in packages endpoint
2 parents 59406a5 + 1edfb93 commit 49052d6

File tree

4 files changed

+204
-15
lines changed

4 files changed

+204
-15
lines changed

CHANGELOG.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ Version v30.0.0
4545
another aliases/ endpoint to lookup for vulnerabilities by aliases. These two endpoints will be
4646
replaced by query parameters on the main vulnerabilities/ endpoint when stabilized.
4747

48+
- Added filters for vulnerabilities endpoint to get fixed packages in accordance to the details given in filters:
49+
For example:
50+
- /api/vulnerabilities?type=pypi&namespace=foo&name=bar
51+
will give only fixed versioned purls of this type `pkg:pypi/foo/bar`
52+
53+
- Package endpoint will give fixed packages of only those that
54+
matches type, name, namespace, subpath and qualifiers of the package queried.
4855

4956
Other:
5057

vulnerabilities/api.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from urllib.parse import unquote
1111

12+
from django.db.models import Prefetch
1213
from django_filters import rest_framework as filters
1314
from packageurl import PackageURL
1415
from rest_framework import serializers
@@ -50,7 +51,7 @@ class Meta:
5051
fields = ["url", "purl"]
5152

5253

53-
class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
54+
class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer):
5455
"""
5556
Used for nesting inside package focused APIs.
5657
"""
@@ -62,6 +63,31 @@ class Meta:
6263
fields = ["url", "vulnerability_id", "summary", "references"]
6364

6465

66+
class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
67+
"""
68+
Used for nesting inside package focused APIs.
69+
"""
70+
71+
class Meta:
72+
model = Vulnerability
73+
fields = ["url", "vulnerability_id"]
74+
75+
76+
class PackageSerializerFixedVulns(serializers.HyperlinkedModelSerializer):
77+
"""
78+
Used for nesting inside vulnerability focused APIs.
79+
"""
80+
81+
purl = serializers.CharField(source="package_url")
82+
fixing_vulnerabilities = MinimalVulnerabilitySerializer(
83+
many=True, source="resolved_to", read_only=True
84+
)
85+
86+
class Meta:
87+
model = Package
88+
fields = ["url", "purl", "fixing_vulnerabilities"]
89+
90+
6591
class AliasSerializer(serializers.HyperlinkedModelSerializer):
6692
"""
6793
Used for nesting inside package focused APIs.
@@ -74,7 +100,9 @@ class Meta:
74100

75101
class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
76102

77-
fixed_packages = MinimalPackageSerializer(many=True, source="resolved_to", read_only=True)
103+
fixed_packages = MinimalPackageSerializer(
104+
many=True, source="filtered_fixed_packages", read_only=True
105+
)
78106
affected_packages = MinimalPackageSerializer(many=True, source="vulnerable_to", read_only=True)
79107

80108
references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set")
@@ -100,12 +128,13 @@ def to_representation(self, instance):
100128
return data
101129

102130
purl = serializers.CharField(source="package_url")
103-
affected_by_vulnerabilities = MinimalVulnerabilitySerializer(
131+
affected_by_vulnerabilities = VulnSerializerRefsAndSummary(
104132
many=True, source="vulnerable_to", read_only=True
105133
)
106-
fixing_vulnerabilities = MinimalVulnerabilitySerializer(
134+
fixing_vulnerabilities = VulnSerializerRefsAndSummary(
107135
many=True, source="resolved_to", read_only=True
108136
)
137+
fixed_packages = PackageSerializerFixedVulns(many=True, read_only=True)
109138

110139
class Meta:
111140
model = Package
@@ -119,6 +148,7 @@ class Meta:
119148
"qualifiers",
120149
"subpath",
121150
"affected_by_vulnerabilities",
151+
"fixed_packages",
122152
"fixing_vulnerabilities",
123153
]
124154

@@ -128,7 +158,7 @@ class PackageFilterSet(filters.FilterSet):
128158

129159
class Meta:
130160
model = Package
131-
fields = ["name", "type", "version", "subpath", "purl"]
161+
fields = ["name", "type", "version", "subpath", "purl", "packagerelatedvulnerability__fix"]
132162

133163
def filter_purl(self, queryset, name, value):
134164
purl = unquote(value)
@@ -193,7 +223,34 @@ class Meta:
193223

194224

195225
class VulnerabilityViewSet(viewsets.ReadOnlyModelViewSet):
196-
queryset = Vulnerability.objects.all()
226+
def get_fixed_packages_qs(self):
227+
"""
228+
Filter the packages that fixes a vulnerability
229+
on fields like name, namespace and type.
230+
"""
231+
package_filter_data = {"packagerelatedvulnerability__fix": True}
232+
233+
query_params = self.request.query_params
234+
for field_name in ["name", "namespace", "type"]:
235+
value = query_params.get(field_name)
236+
if value:
237+
package_filter_data[field_name] = value
238+
239+
return PackageFilterSet(package_filter_data).qs
240+
241+
def get_queryset(self):
242+
"""
243+
Assign filtered packages queryset from `get_fixed_packages_qs`
244+
to a custom attribute `filtered_fixed_packages`
245+
"""
246+
return Vulnerability.objects.prefetch_related(
247+
Prefetch(
248+
"packages",
249+
queryset=self.get_fixed_packages_qs(),
250+
to_attr="filtered_fixed_packages",
251+
)
252+
)
253+
197254
serializer_class = VulnerabilitySerializer
198255
paginate_by = 50
199256
filter_backends = (filters.DjangoFilterBackend,)

vulnerabilities/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,20 @@ def resolved_to(self):
193193
"""
194194
return self.vulnerabilities.filter(packagerelatedvulnerability__fix=True)
195195

196+
@property
197+
def fixed_packages(self):
198+
"""
199+
Returns vulnerabilities which are affecting this package.
200+
"""
201+
return Package.objects.filter(
202+
name=self.name,
203+
namespace=self.namespace,
204+
type=self.type,
205+
qualifiers=self.qualifiers,
206+
subpath=self.subpath,
207+
packagerelatedvulnerability__fix=True,
208+
).distinct()
209+
196210
def set_package_url(self, package_url):
197211
"""
198212
Set each field values to the values of the provided `package_url` string

vulnerabilities/tests/test_fix_api.py

Lines changed: 120 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from vulnerabilities.models import Alias
1515
from vulnerabilities.models import Package
16+
from vulnerabilities.models import PackageRelatedVulnerability
1617
from vulnerabilities.models import Vulnerability
1718
from vulnerabilities.models import VulnerabilityReference
1819
from vulnerabilities.models import VulnerabilityRelatedReference
@@ -25,6 +26,12 @@ def setUp(self):
2526
summary=str(i),
2627
)
2728
self.vulnerability = Vulnerability.objects.create(summary="test")
29+
self.pkg1 = Package.objects.create(name="flask", type="pypi", version="0.1.2")
30+
self.pkg2 = Package.objects.create(name="flask", type="debian", version="0.1.2")
31+
for pkg in [self.pkg1, self.pkg2]:
32+
PackageRelatedVulnerability.objects.create(
33+
package=pkg, vulnerability=self.vulnerability, fix=True
34+
)
2835

2936
def test_api_status(self):
3037
response = self.client.get("/api/vulnerabilities/", format="json")
@@ -43,33 +50,76 @@ def test_api_with_single_vulnerability(self):
4350
"vulnerability_id": f"VULCOID-{int_to_base36(self.vulnerability.id).upper()}",
4451
"summary": "test",
4552
"aliases": [],
46-
"fixed_packages": [],
53+
"fixed_packages": [
54+
{
55+
"url": f"http://testserver/api/packages/{self.pkg1.id}",
56+
"purl": "pkg:pypi/[email protected]",
57+
},
58+
{
59+
"url": f"http://testserver/api/packages/{self.pkg2.id}",
60+
"purl": "pkg:debian/[email protected]",
61+
},
62+
],
63+
"affected_packages": [],
64+
"references": [],
65+
}
66+
67+
def test_api_with_single_vulnerability_with_filters(self):
68+
response = self.client.get(
69+
f"/api/vulnerabilities/{self.vulnerability.id}?type=pypi", format="json"
70+
).data
71+
assert response == {
72+
"url": f"http://testserver/api/vulnerabilities/{self.vulnerability.id}",
73+
"vulnerability_id": f"VULCOID-{int_to_base36(self.vulnerability.id).upper()}",
74+
"summary": "test",
75+
"aliases": [],
76+
"fixed_packages": [
77+
{
78+
"url": f"http://testserver/api/packages/{self.pkg1.id}",
79+
"purl": "pkg:pypi/[email protected]",
80+
},
81+
],
4782
"affected_packages": [],
4883
"references": [],
4984
}
5085

5186

5287
class APITestCasePackage(TestCase):
5388
def setUp(self):
89+
vuln = Vulnerability.objects.create(
90+
summary="test-vuln",
91+
)
92+
self.vuln = vuln
5493
for i in range(0, 10):
5594
query_kwargs = dict(
5695
type="generic",
5796
namespace="nginx",
58-
name=f"test-{i}",
97+
name="test",
5998
version=str(i),
6099
qualifiers={},
61100
subpath="",
62101
)
63-
Package.objects.create(**query_kwargs)
102+
vuln_package = Package.objects.create(**query_kwargs)
103+
PackageRelatedVulnerability.objects.create(
104+
package=vuln_package,
105+
vulnerability=vuln,
106+
fix=False,
107+
)
108+
self.vuln_package = vuln_package
64109
query_kwargs = dict(
65110
type="generic",
66111
namespace="nginx",
67-
name="test-vulnDB",
68-
version="1.0",
112+
name="test",
113+
version="11",
69114
qualifiers={},
70115
subpath="",
71116
)
72117
self.package = Package.objects.create(**query_kwargs)
118+
PackageRelatedVulnerability.objects.create(
119+
package=self.package,
120+
vulnerability=vuln,
121+
fix=True,
122+
)
73123

74124
def test_api_status(self):
75125
response = self.client.get("/api/packages/", format="json")
@@ -79,19 +129,80 @@ def test_api_response(self):
79129
response = self.client.get("/api/packages/", format="json").data
80130
self.assertEqual(response["count"], 11)
81131

82-
def test_api_with_single_vulnerability(self):
132+
def test_api_with_single_vulnerability_and_fixed_package(self):
83133
response = self.client.get(f"/api/packages/{self.package.id}", format="json").data
84134
assert response == {
85135
"url": f"http://testserver/api/packages/{self.package.id}",
86-
"purl": "pkg:generic/nginx/test[email protected]",
136+
"purl": "pkg:generic/nginx/test@11",
87137
"type": "generic",
88138
"namespace": "nginx",
89-
"name": "test-vulnDB",
90-
"version": "1.0",
139+
"name": "test",
140+
"version": "11",
91141
"unresolved_vulnerabilities": [],
92142
"qualifiers": {},
93143
"subpath": "",
144+
"fixed_packages": [
145+
{
146+
"url": f"http://testserver/api/packages/{self.package.id}",
147+
"purl": "pkg:generic/nginx/test@11",
148+
"fixing_vulnerabilities": [
149+
{
150+
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
151+
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
152+
}
153+
],
154+
}
155+
],
94156
"affected_by_vulnerabilities": [],
157+
"fixing_vulnerabilities": [
158+
{
159+
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
160+
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
161+
"summary": "test-vuln",
162+
"references": [],
163+
}
164+
],
165+
}
166+
167+
def test_api_with_single_vulnerability_and_vulnerable_package(self):
168+
response = self.client.get(f"/api/packages/{self.vuln_package.id}", format="json").data
169+
assert response == {
170+
"url": f"http://testserver/api/packages/{self.vuln_package.id}",
171+
"purl": "pkg:generic/nginx/test@9",
172+
"type": "generic",
173+
"namespace": "nginx",
174+
"name": "test",
175+
"version": "9",
176+
"unresolved_vulnerabilities": [
177+
{
178+
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
179+
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
180+
"summary": "test-vuln",
181+
"references": [],
182+
}
183+
],
184+
"qualifiers": {},
185+
"subpath": "",
186+
"fixed_packages": [
187+
{
188+
"url": f"http://testserver/api/packages/{self.package.id}",
189+
"purl": "pkg:generic/nginx/test@11",
190+
"fixing_vulnerabilities": [
191+
{
192+
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
193+
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
194+
}
195+
],
196+
}
197+
],
198+
"affected_by_vulnerabilities": [
199+
{
200+
"url": f"http://testserver/api/vulnerabilities/{self.vuln.id}",
201+
"vulnerability_id": f"VULCOID-{int_to_base36(self.vuln.id).upper()}",
202+
"summary": "test-vuln",
203+
"references": [],
204+
}
205+
],
95206
"fixing_vulnerabilities": [],
96207
}
97208

0 commit comments

Comments
 (0)