Skip to content

Commit 031b2c2

Browse files
committed
Add calculate_version_rank on Package
Signed-off-by: Tushar Goel <tushar.goel.dav@gmail.com>
1 parent 70d298e commit 031b2c2

4 files changed

Lines changed: 106 additions & 24 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 4.2.16 on 2024-12-05 09:37
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [
9+
("vulnerabilities", "0084_alter_package_options_package_version_rank"),
10+
]
11+
12+
operations = [
13+
migrations.AlterField(
14+
model_name="package",
15+
name="version_rank",
16+
field=models.FloatField(
17+
default=0,
18+
help_text="Rank of the version to support ordering by version. Rank zero means the rank has not been defined yet",
19+
),
20+
),
21+
]

vulnerabilities/models.py

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -705,7 +705,7 @@ class Package(PackageURLMixin):
705705
"indicate greater vulnerability risk for the package.",
706706
)
707707

708-
version_rank = models.IntegerField(
708+
version_rank = models.FloatField(
709709
help_text="Rank of the version to support ordering by version. Rank "
710710
"zero means the rank has not been defined yet",
711711
default=0,
@@ -749,6 +749,65 @@ class Meta:
749749
def __str__(self):
750750
return self.package_url
751751

752+
@property
753+
def calculate_version_rank(self):
754+
"""
755+
Calculate and return the `version_rank` for a package that does not have one.
756+
If this package already has a `version_rank`, return it.
757+
758+
The calculated rank will be interpolated between two packages that have
759+
`version_rank` values and are closest to this package in terms of version order.
760+
"""
761+
762+
if self.version_rank > 0:
763+
return self.version_rank
764+
765+
# Determine the version_class for this package's type
766+
version_class = RANGE_CLASS_BY_SCHEMES.get(self.type).version_class
767+
if not version_class:
768+
raise ValueError(f"No version_class defined for package type {self.type}")
769+
770+
group_packages = Package.objects.filter(
771+
type=self.type,
772+
namespace=self.namespace,
773+
name=self.name,
774+
)
775+
776+
# if all packages have version rank 0
777+
778+
if all(p.version_rank == 0 for p in group_packages):
779+
sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version))
780+
for rank, package in enumerate(sorted_packages):
781+
package.version_rank = rank
782+
Package.objects.bulk_update(sorted_packages, fields=["version_rank"])
783+
return self.version_rank
784+
785+
group_packages = group_packages.exclude(version_rank=0)
786+
sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version))
787+
current_version = version_class(self.version)
788+
789+
lower_package, higher_package = None, None
790+
for package in sorted_packages:
791+
package_version = version_class(package.version)
792+
if package_version < current_version:
793+
lower_package = package
794+
elif package_version > current_version:
795+
higher_package = package
796+
break
797+
798+
if lower_package and higher_package:
799+
# Interpolate rank between neighbors
800+
return (lower_package.version_rank + higher_package.version_rank) / 2
801+
elif lower_package:
802+
# If only lower neighbor exists, assign a rank slightly higher than the lower neighbor
803+
return lower_package.version_rank + 1
804+
elif higher_package:
805+
# If only higher neighbor exists, assign a rank slightly lower than the higher neighbor
806+
return higher_package.version_rank - 1
807+
else:
808+
# No neighbors with version_rank; return default rank (e.g., 0)
809+
return 0
810+
752811
@property
753812
def affected_by(self):
754813
"""
@@ -795,14 +854,6 @@ def get_details_url(self, request):
795854

796855
return reverse("package_details", kwargs={"purl": self.purl}, request=request)
797856

798-
def sort_by_version(self, packages):
799-
"""
800-
Return a sequence of `packages` sorted by version.
801-
"""
802-
if not packages:
803-
return []
804-
return sorted(packages, key=lambda x: self.version_class(x.version))
805-
806857
@cached_property
807858
def version_class(self):
808859
range_class = RANGE_CLASS_BY_SCHEMES.get(self.type)
@@ -837,19 +888,21 @@ def get_non_vulnerable_versions(self):
837888
Return a tuple of the next and latest non-vulnerable versions as Package instance.
838889
Return a tuple of (None, None) if there is no non-vulnerable version.
839890
"""
891+
if self.version_rank == 0:
892+
self.calculate_version_rank
840893
non_vulnerable_versions = Package.objects.get_fixed_by_package_versions(
841894
self, fix=False
842895
).only_non_vulnerable()
843-
sorted_versions = self.sort_by_version(non_vulnerable_versions)
896+
sorted_versions = non_vulnerable_versions
844897

845-
later_non_vulnerable_versions = [
846-
non_vuln_ver
847-
for non_vuln_ver in sorted_versions
848-
if self.version_class(non_vuln_ver.version) > self.current_version
849-
]
898+
later_non_vulnerable_versions = non_vulnerable_versions.filter(
899+
version_rank__gt=self.version_rank
900+
)
901+
902+
later_non_vulnerable_versions = list(later_non_vulnerable_versions)
850903

851904
if later_non_vulnerable_versions:
852-
sorted_versions = self.sort_by_version(later_non_vulnerable_versions)
905+
sorted_versions = later_non_vulnerable_versions
853906
next_non_vulnerable = sorted_versions[0]
854907
latest_non_vulnerable = sorted_versions[-1]
855908
return next_non_vulnerable, latest_non_vulnerable
@@ -878,6 +931,8 @@ def get_affecting_vulnerabilities(self):
878931
Return a list of vulnerabilities that affect this package together with information regarding
879932
the versions that fix the vulnerabilities.
880933
"""
934+
if self.version_rank == 0:
935+
self.calculate_version_rank
881936
package_details_vulns = []
882937

883938
fixed_by_packages = Package.objects.get_fixed_by_package_versions(self, fix=True)
@@ -901,12 +956,13 @@ def get_affecting_vulnerabilities(self):
901956
if fixed_version > self.current_version:
902957
later_fixed_packages.append(fixed_pkg)
903958

904-
next_fixed_package = None
905959
next_fixed_package_vulns = []
906960

907961
sort_fixed_by_packages_by_version = []
908962
if later_fixed_packages:
909-
sort_fixed_by_packages_by_version = self.sort_by_version(later_fixed_packages)
963+
sort_fixed_by_packages_by_version = sorted(
964+
later_fixed_packages, key=lambda p: p.version_rank
965+
)
910966

911967
fixed_by_pkgs = []
912968

@@ -936,6 +992,7 @@ def fixing_vulnerabilities(self):
936992
"""
937993
Return a queryset of Vulnerabilities that are fixed by this package.
938994
"""
995+
print("A")
939996
return self.fixed_by_vulnerabilities.all()
940997

941998
@property

vulnerabilities/tests/test_api.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -489,6 +489,7 @@ def setUp(self):
489489
self.pkg_2_14_0_rc1 = from_purl(
490490
"pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1"
491491
)
492+
self.pkg_2_12_6.calculate_version_rank
492493

493494
set_as_fixing(package=self.pkg_2_12_6, vulnerability=self.vul3)
494495

@@ -526,14 +527,14 @@ def test_api_packages_single_with_purl_in_query_num_queries(self):
526527
self.csrf_client.get(f"/api/packages/?purl={self.pkg_2_14_0_rc1.purl}", format="json")
527528

528529
def test_api_packages_single_with_purl_no_version_in_query_num_queries(self):
529-
with self.assertNumQueries(64):
530+
with self.assertNumQueries(68):
530531
self.csrf_client.get(
531532
f"/api/packages/?purl=pkg:maven/com.fasterxml.jackson.core/jackson-databind",
532533
format="json",
533534
)
534535

535536
def test_api_packages_bulk_search(self):
536-
with self.assertNumQueries(45):
537+
with self.assertNumQueries(49):
537538
packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1]
538539
purls = [p.purl for p in packages]
539540

@@ -546,7 +547,7 @@ def test_api_packages_bulk_search(self):
546547
).json()
547548

548549
def test_api_packages_with_lookup(self):
549-
with self.assertNumQueries(14):
550+
with self.assertNumQueries(18):
550551
data = {"purl": self.pkg_2_12_6.purl}
551552

552553
resp = self.csrf_client.post(
@@ -556,7 +557,7 @@ def test_api_packages_with_lookup(self):
556557
).json()
557558

558559
def test_api_packages_bulk_lookup(self):
559-
with self.assertNumQueries(45):
560+
with self.assertNumQueries(49):
560561
packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1]
561562
purls = [p.purl for p in packages]
562563

vulnerabilities/tests/test_models.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -423,8 +423,11 @@ def test_sort_by_version(self):
423423
version="3.0.0",
424424
)
425425

426-
sorted_pkgs = requesting_package.sort_by_version(vuln_pkg_list)
427-
first_sorted_item = sorted_pkgs[0]
426+
requesting_package.calculate_version_rank
427+
428+
sorted_pkgs = Package.objects.filter(package_url__in=list_to_sort)
429+
430+
sorted_pkgs = list(sorted_pkgs)
428431

429432
assert sorted_pkgs[0].purl == "pkg:npm/sequelize@3.9.1"
430433
assert sorted_pkgs[-1].purl == "pkg:npm/sequelize@3.40.1"

0 commit comments

Comments
 (0)