Skip to content

Commit 4e5ef60

Browse files
authored
Add resource URL to the vulnerability and package details view in the API serializers (#1423)
* Add vulnerability_url in API Signed-off-by: Tushar Goel <[email protected]> * Add package_url in API Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> * Address review comments Signed-off-by: Tushar Goel <[email protected]> * Address review comments Signed-off-by: Tushar Goel <[email protected]> * Fix tests Signed-off-by: Tushar Goel <[email protected]> --------- Signed-off-by: Tushar Goel <[email protected]>
1 parent 30d3357 commit 4e5ef60

File tree

3 files changed

+157
-48
lines changed

3 files changed

+157
-48
lines changed

vulnerabilities/api.py

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
from rest_framework import viewsets
2121
from rest_framework.decorators import action
2222
from rest_framework.response import Response
23+
from rest_framework.reverse import reverse
2324
from rest_framework.throttling import AnonRateThrottle
2425
from rest_framework.throttling import UserRateThrottle
2526

@@ -48,7 +49,32 @@ class Meta:
4849
fields = ["reference_url", "reference_id", "scores", "url"]
4950

5051

51-
class MinimalPackageSerializer(serializers.HyperlinkedModelSerializer):
52+
class BaseResourceSerializer(serializers.HyperlinkedModelSerializer):
53+
"""
54+
Base serializer containing common methods.
55+
"""
56+
57+
def get_fields(self):
58+
fields = super().get_fields()
59+
fields["resource_url"] = serializers.SerializerMethodField(method_name="get_resource_url")
60+
return fields
61+
62+
def get_resource_url(self, instance):
63+
"""
64+
Return the instance fully qualified URL including the schema and domain.
65+
66+
Usage:
67+
resource_url = serializers.SerializerMethodField()
68+
"""
69+
resource_url = instance.get_absolute_url()
70+
71+
if request := self.context.get("request", None):
72+
return request.build_absolute_uri(location=resource_url)
73+
74+
return resource_url
75+
76+
77+
class MinimalPackageSerializer(BaseResourceSerializer):
5278
"""
5379
Used for nesting inside vulnerability focused APIs.
5480
"""
@@ -79,7 +105,7 @@ class Meta:
79105
fields = ["url", "purl", "is_vulnerable", "affected_by_vulnerabilities"]
80106

81107

82-
class MinimalVulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
108+
class MinimalVulnerabilitySerializer(BaseResourceSerializer):
83109
"""
84110
Lookup vulnerabilities by aliases (such as a CVE).
85111
"""
@@ -99,7 +125,7 @@ class Meta:
99125
fields = ["alias"]
100126

101127

102-
class VulnSerializerRefsAndSummary(serializers.HyperlinkedModelSerializer):
128+
class VulnSerializerRefsAndSummary(BaseResourceSerializer):
103129
"""
104130
Lookup vulnerabilities references by aliases (such as a CVE).
105131
"""
@@ -141,7 +167,7 @@ def to_representation(self, instance):
141167
return representation
142168

143169

144-
class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
170+
class VulnerabilitySerializer(BaseResourceSerializer):
145171
fixed_packages = MinimalPackageSerializer(
146172
many=True, source="filtered_fixed_packages", read_only=True
147173
)
@@ -152,13 +178,12 @@ class VulnerabilitySerializer(serializers.HyperlinkedModelSerializer):
152178
weaknesses = WeaknessSerializer(many=True)
153179

154180
def to_representation(self, instance):
155-
representation = super().to_representation(instance)
181+
data = super().to_representation(instance)
156182

157-
# Exclude None values from the weaknesses list
158-
weaknesses = representation.get("weaknesses", [])
159-
representation["weaknesses"] = [weakness for weakness in weaknesses if weakness is not None]
183+
weaknesses = data.get("weaknesses", [])
184+
data["weaknesses"] = [weakness for weakness in weaknesses if weakness is not None]
160185

161-
return representation
186+
return data
162187

163188
class Meta:
164189
model = Vulnerability
@@ -174,14 +199,15 @@ class Meta:
174199
]
175200

176201

177-
class PackageSerializer(serializers.HyperlinkedModelSerializer):
202+
class PackageSerializer(BaseResourceSerializer):
178203
"""
179204
Lookup software package using Package URLs
180205
"""
181206

182207
def to_representation(self, instance):
183208
data = super().to_representation(instance)
184209
data["qualifiers"] = normalize_qualifiers(data["qualifiers"], encode=False)
210+
185211
return data
186212

187213
next_non_vulnerable_version = serializers.SerializerMethodField("get_next_non_vulnerable")

vulnerabilities/models.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,18 @@ def get_absolute_url(self):
257257
"""
258258
return reverse("vulnerability_details", args=[self.vulnerability_id])
259259

260+
def get_details_url(self, request):
261+
"""
262+
Return this Package details URL.
263+
"""
264+
from rest_framework.reverse import reverse
265+
266+
return reverse(
267+
"vulnerability_details",
268+
kwargs={"vulnerability_id": self.vulnerability_id},
269+
request=request,
270+
)
271+
260272
def get_related_cpes(self):
261273
"""
262274
Return a list of CPE strings of this vulnerability.
@@ -633,6 +645,14 @@ def get_absolute_url(self):
633645
"""
634646
return reverse("package_details", args=[self.purl])
635647

648+
def get_details_url(self, request):
649+
"""
650+
Return this Package details URL.
651+
"""
652+
from rest_framework.reverse import reverse
653+
654+
return reverse("package_details", kwargs={"purl": self.purl}, request=request)
655+
636656
def sort_by_version(self, packages):
637657
"""
638658
Return a list of `packages` sorted by version.

vulnerabilities/tests/test_api.py

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

1010
import json
1111
import os
12+
from collections import OrderedDict
1213
from urllib.parse import quote
1314

1415
from django.test import TestCase
@@ -223,18 +224,21 @@ def test_api_with_single_vulnerability(self):
223224
"vulnerability_id": self.vulnerability.vulnerability_id,
224225
"summary": "test",
225226
"aliases": [],
227+
"resource_url": f"http://testserver/vulnerabilities/{self.vulnerability.vulnerability_id}",
226228
"fixed_packages": [
227229
{
228230
"url": f"http://testserver/api/packages/{self.pkg2.id}",
229231
"purl": "pkg:deb/[email protected]",
230232
"is_vulnerable": False,
231233
"affected_by_vulnerabilities": [],
234+
"resource_url": f"http://testserver/packages/{self.pkg2.purl}",
232235
},
233236
{
234237
"url": f"http://testserver/api/packages/{self.pkg1.id}",
235238
"purl": "pkg:pypi/[email protected]",
236239
"is_vulnerable": False,
237240
"affected_by_vulnerabilities": [],
241+
"resource_url": f"http://testserver/packages/{self.pkg1.purl}",
238242
},
239243
],
240244
"affected_packages": [],
@@ -257,11 +261,13 @@ def test_api_with_single_vulnerability_with_filters(self):
257261
"vulnerability_id": self.vulnerability.vulnerability_id,
258262
"summary": "test",
259263
"aliases": [],
264+
"resource_url": f"http://testserver/vulnerabilities/{self.vulnerability.vulnerability_id}",
260265
"fixed_packages": [
261266
{
262267
"url": f"http://testserver/api/packages/{self.pkg1.id}",
263268
"purl": "pkg:pypi/[email protected]",
264269
"is_vulnerable": False,
270+
"resource_url": f"http://testserver/packages/{self.pkg1.purl}",
265271
"affected_by_vulnerabilities": [],
266272
},
267273
],
@@ -443,49 +449,106 @@ def test_api_with_lesser_and_greater_fixed_by_packages(self):
443449
"next_non_vulnerable_version": "2.14.0-rc1",
444450
"latest_non_vulnerable_version": "2.14.0-rc1",
445451
"affected_by_vulnerabilities": [
446-
{
447-
"url": f"http://testserver/api/vulnerabilities/{self.vuln_VCID_2nyb_8rwu_aaag.id}",
448-
"vulnerability_id": "VCID-2nyb-8rwu-aaag",
449-
"summary": "This is VCID-2nyb-8rwu-aaag",
450-
"references": [],
451-
"fixed_packages": [
452-
{
453-
"url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_2.id}",
454-
"purl": "pkg:maven/com.fasterxml.jackson.core/[email protected]",
455-
"is_vulnerable": True,
456-
"affected_by_vulnerabilities": [
457-
{"vulnerability": "VCID-gqhw-ngh8-aaap"}
452+
OrderedDict(
453+
[
454+
(
455+
"url",
456+
f"http://testserver/api/vulnerabilities/{self.vuln_VCID_2nyb_8rwu_aaag.id}",
457+
),
458+
("vulnerability_id", "VCID-2nyb-8rwu-aaag"),
459+
("summary", "This is VCID-2nyb-8rwu-aaag"),
460+
("references", []),
461+
(
462+
"fixed_packages",
463+
[
464+
OrderedDict(
465+
[
466+
(
467+
"url",
468+
f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_2.id}",
469+
),
470+
(
471+
"purl",
472+
"pkg:maven/com.fasterxml.jackson.core/[email protected]",
473+
),
474+
("is_vulnerable", True),
475+
(
476+
"affected_by_vulnerabilities",
477+
[{"vulnerability": "VCID-gqhw-ngh8-aaap"}],
478+
),
479+
(
480+
"resource_url",
481+
"http://testserver/packages/pkg:maven/com.fasterxml.jackson.core/[email protected]",
482+
),
483+
]
484+
)
458485
],
459-
}
460-
],
461-
"aliases": ["CVE-2020-36518", "GHSA-57j2-w4cx-62h2"],
462-
}
486+
),
487+
("aliases", ["CVE-2020-36518", "GHSA-57j2-w4cx-62h2"]),
488+
("resource_url", "http://testserver/vulnerabilities/VCID-2nyb-8rwu-aaag"),
489+
]
490+
)
463491
],
464492
"fixing_vulnerabilities": [
465-
{
466-
"url": f"http://testserver/api/vulnerabilities/{self.vuln_VCID_ftmk_wbwx_aaar.id}",
467-
"vulnerability_id": "VCID-ftmk-wbwx-aaar",
468-
"summary": "This is VCID-ftmk-wbwx-aaar",
469-
"references": [],
470-
"fixed_packages": [
471-
{
472-
"url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_12_6.id}",
473-
"purl": "pkg:maven/com.fasterxml.jackson.core/[email protected]",
474-
"is_vulnerable": False,
475-
"affected_by_vulnerabilities": [],
476-
},
477-
{
478-
"url": f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_1.id}",
479-
"purl": "pkg:maven/com.fasterxml.jackson.core/[email protected]",
480-
"is_vulnerable": True,
481-
"affected_by_vulnerabilities": [
482-
{"vulnerability": "VCID-2nyb-8rwu-aaag"}
493+
OrderedDict(
494+
[
495+
(
496+
"url",
497+
f"http://testserver/api/vulnerabilities/{self.vuln_VCID_ftmk_wbwx_aaar.id}",
498+
),
499+
("vulnerability_id", "VCID-ftmk-wbwx-aaar"),
500+
("summary", "This is VCID-ftmk-wbwx-aaar"),
501+
("references", []),
502+
(
503+
"fixed_packages",
504+
[
505+
OrderedDict(
506+
[
507+
(
508+
"url",
509+
f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_12_6.id}",
510+
),
511+
(
512+
"purl",
513+
"pkg:maven/com.fasterxml.jackson.core/[email protected]",
514+
),
515+
("is_vulnerable", False),
516+
("affected_by_vulnerabilities", []),
517+
(
518+
"resource_url",
519+
"http://testserver/packages/pkg:maven/com.fasterxml.jackson.core/[email protected]",
520+
),
521+
]
522+
),
523+
OrderedDict(
524+
[
525+
(
526+
"url",
527+
f"http://testserver/api/packages/{self.package_maven_jackson_databind_2_13_1.id}",
528+
),
529+
(
530+
"purl",
531+
"pkg:maven/com.fasterxml.jackson.core/[email protected]",
532+
),
533+
("is_vulnerable", True),
534+
(
535+
"affected_by_vulnerabilities",
536+
[{"vulnerability": "VCID-2nyb-8rwu-aaag"}],
537+
),
538+
(
539+
"resource_url",
540+
"http://testserver/packages/pkg:maven/com.fasterxml.jackson.core/[email protected]",
541+
),
542+
]
543+
),
483544
],
484-
},
485-
],
486-
"aliases": ["CVE-2021-46877", "GHSA-3x8x-79m2-3w2w"],
487-
},
545+
),
546+
("aliases", ["CVE-2021-46877", "GHSA-3x8x-79m2-3w2w"]),
547+
("resource_url", "http://testserver/vulnerabilities/VCID-ftmk-wbwx-aaar"),
548+
]
549+
)
488550
],
551+
"resource_url": "http://testserver/packages/pkg:maven/com.fasterxml.jackson.core/[email protected]",
489552
}
490553

491554
assert response == expected_response

0 commit comments

Comments
 (0)