Skip to content

Commit 70a5266

Browse files
authored
Merge pull request #1558 from aboutcode-org/add-tests-for-queries
Enhance API performace
2 parents 76428da + 7c4a8f0 commit 70a5266

File tree

4 files changed

+197
-54
lines changed

4 files changed

+197
-54
lines changed

vulnerabilities/api.py

Lines changed: 23 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -87,29 +87,23 @@ def get_resource_url(self, instance):
8787
return resource_url
8888

8989

90-
class MinimalPackageSerializer(BaseResourceSerializer):
90+
class VulnVulnIDSerializer(serializers.Serializer):
9191
"""
92-
Used for nesting inside vulnerability focused APIs.
92+
Serializer for the series of vulnerability IDs.
9393
"""
9494

95-
def get_affected_vulnerabilities(self, package):
96-
parent_affected_vulnerabilities = package.fixed_package_details.get("vulnerabilities") or []
97-
98-
affected_vulnerabilities = [
99-
self.get_vulnerability(vuln) for vuln in parent_affected_vulnerabilities
100-
]
95+
vulnerability = serializers.CharField(source="vulnerability_id")
10196

102-
return affected_vulnerabilities
97+
class Meta:
98+
fields = ["vulnerability"]
10399

104-
def get_vulnerability(self, vuln):
105-
affected_vulnerability = {}
106100

107-
vulnerability = vuln.get("vulnerability")
108-
if vulnerability:
109-
affected_vulnerability["vulnerability"] = vulnerability.vulnerability_id
110-
return affected_vulnerability
101+
class MinimalPackageSerializer(BaseResourceSerializer):
102+
"""
103+
Used for nesting inside vulnerability focused APIs.
104+
"""
111105

112-
affected_by_vulnerabilities = serializers.SerializerMethodField("get_affected_vulnerabilities")
106+
affected_by_vulnerabilities = VulnVulnIDSerializer(source="affecting_vulns", many=True)
113107

114108
purl = serializers.CharField(source="package_url")
115109

@@ -145,18 +139,17 @@ class VulnSerializerRefsAndSummary(BaseResourceSerializer):
145139
Lookup vulnerabilities references by aliases (such as a CVE).
146140
"""
147141

148-
def to_representation(self, instance):
149-
data = super().to_representation(instance)
150-
aliases = [alias["alias"] for alias in data["aliases"]]
151-
data["aliases"] = aliases
152-
return data
153-
154142
fixed_packages = MinimalPackageSerializer(
155143
many=True, source="filtered_fixed_packages", read_only=True
156144
)
157145

158146
references = VulnerabilityReferenceSerializer(many=True, source="vulnerabilityreference_set")
159-
aliases = AliasSerializer(many=True, source="alias")
147+
148+
aliases = serializers.SerializerMethodField()
149+
150+
def get_aliases(self, obj):
151+
# Assuming `obj.aliases` is a queryset of `Alias` objects
152+
return [alias.alias for alias in obj.aliases.all()]
160153

161154
class Meta:
162155
model = Vulnerability
@@ -257,34 +250,22 @@ class PackageSerializer(BaseResourceSerializer):
257250
Lookup software package using Package URLs
258251
"""
259252

260-
def to_representation(self, instance):
261-
data = super().to_representation(instance)
262-
data["qualifiers"] = normalize_qualifiers(data["qualifiers"], encode=False)
263-
264-
return data
265-
266-
next_non_vulnerable_version = serializers.SerializerMethodField("get_next_non_vulnerable")
267-
268-
def get_next_non_vulnerable(self, package):
269-
next_non_vulnerable = package.fixed_package_details.get("next_non_vulnerable", None)
270-
if next_non_vulnerable:
271-
return next_non_vulnerable.version
272-
273-
latest_non_vulnerable_version = serializers.SerializerMethodField("get_latest_non_vulnerable")
274-
275-
def get_latest_non_vulnerable(self, package):
276-
latest_non_vulnerable = package.fixed_package_details.get("latest_non_vulnerable", None)
277-
if latest_non_vulnerable:
278-
return latest_non_vulnerable.version
253+
next_non_vulnerable_version = serializers.CharField(read_only=True)
254+
latest_non_vulnerable_version = serializers.CharField(read_only=True)
279255

280256
purl = serializers.CharField(source="package_url")
281257

282258
affected_by_vulnerabilities = serializers.SerializerMethodField("get_affected_vulnerabilities")
283259

284260
fixing_vulnerabilities = serializers.SerializerMethodField("get_fixing_vulnerabilities")
285261

262+
qualifiers = serializers.SerializerMethodField()
263+
286264
is_vulnerable = serializers.BooleanField()
287265

266+
def get_qualifiers(self, package):
267+
return normalize_qualifiers(package.qualifiers, encode=False)
268+
288269
def get_fixed_packages(self, package):
289270
"""
290271
Return a queryset of all packages that fix a vulnerability with
@@ -368,8 +349,6 @@ class Meta:
368349
"fixing_vulnerabilities",
369350
]
370351

371-
is_vulnerable = serializers.BooleanField()
372-
373352

374353
class PackageFilterSet(filters.FilterSet):
375354
purl = filters.CharFilter(method="filter_purl")

vulnerabilities/models.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -711,6 +711,47 @@ def version_class(self):
711711
def current_version(self):
712712
return self.version_class(self.version)
713713

714+
@property
715+
def next_non_vulnerable_version(self):
716+
"""
717+
Return the version string of the next non-vulnerable package version.
718+
"""
719+
next_non_vulnerable, _ = self.get_non_vulnerable_versions()
720+
return next_non_vulnerable.version if next_non_vulnerable else None
721+
722+
@property
723+
def latest_non_vulnerable_version(self):
724+
"""
725+
Return the version string of the latest non-vulnerable package version.
726+
"""
727+
_, latest_non_vulnerable = self.get_non_vulnerable_versions()
728+
return latest_non_vulnerable.version if latest_non_vulnerable else None
729+
730+
def get_non_vulnerable_versions(self):
731+
"""
732+
Return a tuple of the next and latest non-vulnerable versions as PackageURL objects.
733+
Return a tuple of (None, None) if there is no non-vulnerable version.
734+
"""
735+
non_vulnerable_versions = Package.objects.get_fixed_by_package_versions(
736+
self, fix=False
737+
).only_non_vulnerable()
738+
sorted_versions = self.sort_by_version(non_vulnerable_versions)
739+
740+
later_non_vulnerable_versions = [
741+
non_vuln_ver
742+
for non_vuln_ver in sorted_versions
743+
if self.version_class(non_vuln_ver.version) > self.current_version
744+
]
745+
746+
if later_non_vulnerable_versions:
747+
sorted_versions = self.sort_by_version(later_non_vulnerable_versions)
748+
next_non_vulnerable_version = sorted_versions[0]
749+
latest_non_vulnerable_version = sorted_versions[-1]
750+
751+
return next_non_vulnerable_version, latest_non_vulnerable_version
752+
753+
return None, None
754+
714755
@property
715756
def fixed_package_details(self):
716757
"""
@@ -827,6 +868,20 @@ def affecting_vulnerabilities(self):
827868
"""
828869
return self.vulnerabilities.filter(packagerelatedvulnerability__fix=False)
829870

871+
@property
872+
def affecting_vulns(self):
873+
"""
874+
Return a queryset of Vulnerabilities that affect this `package`.
875+
"""
876+
fixed_by_packages = Package.objects.get_fixed_by_package_versions(self, fix=True)
877+
return self.vulnerabilities.affecting_vulnerabilities().prefetch_related(
878+
Prefetch(
879+
"packages",
880+
queryset=fixed_by_packages,
881+
to_attr="fixed_packages",
882+
)
883+
)
884+
830885

831886
class PackageRelatedVulnerability(models.Model):
832887
"""

vulnerabilities/tests/test_api.py

Lines changed: 116 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -388,7 +388,7 @@ def add_aliases(vuln, aliases):
388388
Alias.objects.create(alias=alias, vulnerability=vuln)
389389

390390

391-
class APITestCasePackage(TestCase):
391+
class APIPerformanceTest(TestCase):
392392
def setUp(self):
393393
self.user = ApiUser.objects.create_api_user(username="[email protected]")
394394
self.auth = f"Token {self.user.auth_token.key}"
@@ -439,18 +439,124 @@ def setUp(self):
439439
set_as_affected_by(package=self.pkg_2_13_2, vulnerability=self.vul2)
440440
set_as_fixing(package=self.pkg_2_13_2, vulnerability=self.vul1)
441441

442-
def test_api_with_package_with_no_vulnerabilities(self):
443-
affected_vulnerabilities = []
444-
vuln = {
445-
"foo": "bar",
446-
}
442+
def test_api_packages_all_num_queries(self):
443+
with self.assertNumQueries(4):
444+
# There are 4 queries:
445+
# 1. SAVEPOINT
446+
# 2. Authenticating user
447+
# 3. Get all vulnerable packages
448+
# 4. RELEASE SAVEPOINT
449+
response = self.csrf_client.get(f"/api/packages/all", format="json").data
450+
451+
assert len(response) == 3
452+
assert response == [
453+
"pkg:maven/com.fasterxml.jackson.core/[email protected]",
454+
"pkg:maven/com.fasterxml.jackson.core/[email protected]",
455+
"pkg:maven/com.fasterxml.jackson.core/[email protected]",
456+
]
457+
458+
def test_api_packages_single_num_queries(self):
459+
with self.assertNumQueries(8):
460+
self.csrf_client.get(f"/api/packages/{self.pkg_2_14_0_rc1.id}", format="json")
461+
462+
def test_api_packages_single_with_purl_in_query_num_queries(self):
463+
with self.assertNumQueries(9):
464+
self.csrf_client.get(f"/api/packages/?purl={self.pkg_2_14_0_rc1.purl}", format="json")
465+
466+
def test_api_packages_single_with_purl_no_version_in_query_num_queries(self):
467+
with self.assertNumQueries(64):
468+
self.csrf_client.get(
469+
f"/api/packages/?purl=pkg:maven/com.fasterxml.jackson.core/jackson-databind",
470+
format="json",
471+
)
447472

448-
package_with_no_vulnerabilities = MinimalPackageSerializer.get_vulnerability(
449-
self,
450-
vuln,
473+
def test_api_packages_bulk_search(self):
474+
with self.assertNumQueries(45):
475+
packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1]
476+
purls = [p.purl for p in packages]
477+
478+
data = {"purls": purls, "purl_only": False, "plain_purl": True}
479+
480+
resp = self.csrf_client.post(
481+
f"/api/packages/bulk_search",
482+
data=json.dumps(data),
483+
content_type="application/json",
484+
).json()
485+
486+
def test_api_packages_with_lookup(self):
487+
with self.assertNumQueries(14):
488+
data = {"purl": self.pkg_2_12_6.purl}
489+
490+
resp = self.csrf_client.post(
491+
f"/api/packages/lookup",
492+
data=json.dumps(data),
493+
content_type="application/json",
494+
).json()
495+
496+
def test_api_packages_bulk_lookup(self):
497+
with self.assertNumQueries(45):
498+
packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1]
499+
purls = [p.purl for p in packages]
500+
501+
data = {"purls": purls}
502+
503+
resp = self.csrf_client.post(
504+
f"/api/packages/bulk_lookup",
505+
data=json.dumps(data),
506+
content_type="application/json",
507+
).json()
508+
509+
510+
class APITestCasePackage(TestCase):
511+
def setUp(self):
512+
self.user = ApiUser.objects.create_api_user(username="[email protected]")
513+
self.auth = f"Token {self.user.auth_token.key}"
514+
self.csrf_client = APIClient(enforce_csrf_checks=True)
515+
self.csrf_client.credentials(HTTP_AUTHORIZATION=self.auth)
516+
517+
# This setup creates the following data:
518+
# vulnerabilities: vul1, vul2, vul3
519+
# pkg:maven/com.fasterxml.jackson.core/jackson-databind
520+
# with these versions:
521+
# pkg_2_12_6: @ 2.12.6 affected by fixing vul3
522+
# pkg_2_12_6_1: @ 2.12.6.1 affected by vul2 fixing vul1
523+
# pkg_2_13_1: @ 2.13.1 affected by vul1 fixing vul3
524+
# pkg_2_13_2: @ 2.13.2 affected by vul2 fixing vul1
525+
# pkg_2_14_0_rc1: @ 2.14.0-rc1 affected by fixing
526+
527+
# searched-for pkg's vuln
528+
self.vul1 = create_vuln("VCID-vul1-vul1-vul1", ["CVE-2020-36518", "GHSA-57j2-w4cx-62h2"])
529+
self.vul2 = create_vuln("VCID-vul2-vul2-vul2")
530+
# This is the vuln fixed by the searched-for pkg -- and by a lesser version (created below),
531+
# which WILL be included in the API
532+
self.vul3 = create_vuln("VCID-vul3-vul3-vul3", ["CVE-2021-46877", "GHSA-3x8x-79m2-3w2w"])
533+
534+
from_purl = Package.objects.from_purl
535+
# lesser-version pkg that also fixes the vuln fixed by the searched-for pkg
536+
self.pkg_2_12_6 = from_purl("pkg:maven/com.fasterxml.jackson.core/[email protected]")
537+
# this is a lesser version omitted from the API that fixes searched-for pkg's vuln
538+
self.pkg_2_12_6_1 = from_purl(
539+
"pkg:maven/com.fasterxml.jackson.core/[email protected]"
540+
)
541+
# searched-for pkg
542+
self.pkg_2_13_1 = from_purl("pkg:maven/com.fasterxml.jackson.core/[email protected]")
543+
# this is a greater version that fixes searched-for pkg's vuln
544+
self.pkg_2_13_2 = from_purl("pkg:maven/com.fasterxml.jackson.core/[email protected]")
545+
# This addresses both next and latest non-vulnerable pkg
546+
self.pkg_2_14_0_rc1 = from_purl(
547+
"pkg:maven/com.fasterxml.jackson.core/[email protected]"
451548
)
452549

453-
assert package_with_no_vulnerabilities is None
550+
set_as_fixing(package=self.pkg_2_12_6, vulnerability=self.vul3)
551+
552+
set_as_affected_by(package=self.pkg_2_12_6_1, vulnerability=self.vul2)
553+
set_as_fixing(package=self.pkg_2_12_6_1, vulnerability=self.vul1)
554+
555+
set_as_affected_by(package=self.pkg_2_13_1, vulnerability=self.vul1)
556+
set_as_fixing(package=self.pkg_2_13_1, vulnerability=self.vul3)
557+
558+
set_as_affected_by(package=self.pkg_2_13_2, vulnerability=self.vul2)
559+
set_as_fixing(package=self.pkg_2_13_2, vulnerability=self.vul1)
454560

455561
def test_api_with_lesser_and_greater_fixed_by_packages(self):
456562
response = self.csrf_client.get(f"/api/packages/{self.pkg_2_13_1.id}", format="json").data

vulnerablecode/settings.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,9 @@
287287

288288

289289
if DEBUG_TOOLBAR:
290+
# Uncomment this to get pyinstrument profiles
291+
# PYINSTRUMENT_PROFILE_DIR = "profiles"
292+
290293
INSTALLED_APPS += ("debug_toolbar",)
291294

292295
MIDDLEWARE += (

0 commit comments

Comments
 (0)