From 83b55116fa8741ca862bf37e5dcb627544c97be1 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 4 Dec 2024 17:32:03 +0530 Subject: [PATCH 01/10] Add pipeline to sort packages Signed-off-by: Tushar Goel --- vulnerabilities/improvers/__init__.py | 2 + ...er_package_options_package_version_rank.py | 35 +++++++ vulnerabilities/models.py | 8 +- .../pipelines/compute_package_version.py | 99 +++++++++++++++++++ 4 files changed, 143 insertions(+), 1 deletion(-) create mode 100644 vulnerabilities/migrations/0084_alter_package_options_package_version_rank.py create mode 100644 vulnerabilities/pipelines/compute_package_version.py diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index fd18fb28c..4ef5f0e37 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -11,6 +11,7 @@ from vulnerabilities.improvers import vulnerability_status from vulnerabilities.pipelines import VulnerableCodePipeline from vulnerabilities.pipelines import compute_package_risk +from vulnerabilities.pipelines import compute_package_version from vulnerabilities.pipelines import enhance_with_exploitdb from vulnerabilities.pipelines import enhance_with_kev from vulnerabilities.pipelines import enhance_with_metasploit @@ -39,6 +40,7 @@ enhance_with_metasploit.MetasploitImproverPipeline, enhance_with_exploitdb.ExploitDBImproverPipeline, compute_package_risk.ComputePackageRiskPipeline, + compute_package_version.ComputeVersionRankPipeline, ] IMPROVERS_REGISTRY = { diff --git a/vulnerabilities/migrations/0084_alter_package_options_package_version_rank.py b/vulnerabilities/migrations/0084_alter_package_options_package_version_rank.py new file mode 100644 index 000000000..6b33c1a59 --- /dev/null +++ b/vulnerabilities/migrations/0084_alter_package_options_package_version_rank.py @@ -0,0 +1,35 @@ +# Generated by Django 4.2.16 on 2024-12-04 11:50 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0083_alter_packagechangelog_software_version_and_more"), + ] + + operations = [ + migrations.AlterModelOptions( + name="package", + options={ + "ordering": [ + "type", + "namespace", + "name", + "version_rank", + "version", + "qualifiers", + "subpath", + ] + }, + ), + migrations.AddField( + model_name="package", + name="version_rank", + field=models.IntegerField( + default=0, + help_text="Rank of the version to support ordering by version. Rank zero means the rank has not been defined yet", + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 9cafe6d15..ba265a957 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -705,6 +705,12 @@ class Package(PackageURLMixin): "indicate greater vulnerability risk for the package.", ) + version_rank = models.IntegerField( + help_text="Rank of the version to support ordering by version. Rank " + "zero means the rank has not been defined yet", + default=0, + ) + objects = PackageQuerySet.as_manager() def save(self, *args, **kwargs): @@ -738,7 +744,7 @@ def purl(self): class Meta: unique_together = ["type", "namespace", "name", "version", "qualifiers", "subpath"] - ordering = ["type", "namespace", "name", "version", "qualifiers", "subpath"] + ordering = ["type", "namespace", "name", "version_rank", "version", "qualifiers", "subpath"] def __str__(self): return self.package_url diff --git a/vulnerabilities/pipelines/compute_package_version.py b/vulnerabilities/pipelines/compute_package_version.py new file mode 100644 index 000000000..fd80999b1 --- /dev/null +++ b/vulnerabilities/pipelines/compute_package_version.py @@ -0,0 +1,99 @@ +# +# Copyright (c) nexB Inc. and others. All rights reserved. +# VulnerableCode is a trademark of nexB Inc. +# SPDX-License-Identifier: Apache-2.0 +# See http://www.apache.org/licenses/LICENSE-2.0 for the license text. +# See https://github.com/aboutcode-org/vulnerablecode for support or download. +# See https://aboutcode.org for more information about nexB OSS projects. +# + +from itertools import groupby + +from aboutcode.pipeline import LoopProgress +from django.db import transaction +from univers.version_range import RANGE_CLASS_BY_SCHEMES +from univers.versions import Version + +from vulnerabilities.models import Package +from vulnerabilities.pipelines import VulnerableCodePipeline + + +class ComputeVersionRankPipeline(VulnerableCodePipeline): + """ + A pipeline to compute and assign version ranks for all packages. + """ + + pipeline_id = "compute_version_rank" + license_expression = None + + @classmethod + def steps(cls): + return (cls.compute_and_store_version_rank,) + + def compute_and_store_version_rank(self): + """ + Compute and assign version ranks to all packages. + """ + groups = Package.objects.only("type", "namespace", "name").order_by( + "type", "namespace", "name" + ) + + def key(package): + return package.type, package.namespace, package.name + + groups = groupby(groups, key=key) + + groups = [(list(x), list(y)) for x, y in groups] + + total_groups = len(groups) + self.log(f"Calculating `version_rank` for {total_groups:,d} groups of packages.") + + progress = LoopProgress( + total_iterations=total_groups, + logger=self.log, + progress_step=5, + ) + + for group, packages in progress.iter(groups): + type, namespace, name = group + if type not in RANGE_CLASS_BY_SCHEMES: + continue + self.update_version_rank_for_group(packages) + + self.log("Successfully populated `version_rank` for all packages.") + + @transaction.atomic + def update_version_rank_for_group(self, packages): + """ + Update the `version_rank` for all packages in a specific group. + """ + + # Sort the packages by version + sorted_packages = self.sort_packages_by_version(packages) + + # Assign version ranks + updates = [] + for rank, package in enumerate(sorted_packages): + package.version_rank = rank + updates.append(package) + + # Bulk update to save the ranks + Package.objects.bulk_update(updates, fields=["version_rank"]) + + def sort_packages_by_version(self, packages): + """ + Sort packages by version using `version_class`. + """ + + if not packages: + return [] + version_class = RANGE_CLASS_BY_SCHEMES.get(packages[0].type).version_class + if not version_class: + version_class = Version + return sorted(packages, key=lambda p: version_class(p.version)) + + def log(self, message): + """ + Simple logging function. + """ + print(message) From 2b5fa29879bb6e70854731cfa01d42373cfaff48 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Wed, 4 Dec 2024 17:38:30 +0530 Subject: [PATCH 02/10] Add tests Signed-off-by: Tushar Goel --- vulnerabilities/improvers/__init__.py | 4 +- ...ion.py => compute_package_version_rank.py} | 0 .../test_compute_package_version_rank.py | 59 +++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) rename vulnerabilities/pipelines/{compute_package_version.py => compute_package_version_rank.py} (100%) create mode 100644 vulnerabilities/tests/test_compute_package_version_rank.py diff --git a/vulnerabilities/improvers/__init__.py b/vulnerabilities/improvers/__init__.py index 4ef5f0e37..dd73eb02d 100644 --- a/vulnerabilities/improvers/__init__.py +++ b/vulnerabilities/improvers/__init__.py @@ -11,7 +11,7 @@ from vulnerabilities.improvers import vulnerability_status from vulnerabilities.pipelines import VulnerableCodePipeline from vulnerabilities.pipelines import compute_package_risk -from vulnerabilities.pipelines import compute_package_version +from vulnerabilities.pipelines import compute_package_version_rank from vulnerabilities.pipelines import enhance_with_exploitdb from vulnerabilities.pipelines import enhance_with_kev from vulnerabilities.pipelines import enhance_with_metasploit @@ -40,7 +40,7 @@ enhance_with_metasploit.MetasploitImproverPipeline, enhance_with_exploitdb.ExploitDBImproverPipeline, compute_package_risk.ComputePackageRiskPipeline, - compute_package_version.ComputeVersionRankPipeline, + compute_package_version_rank.ComputeVersionRankPipeline, ] IMPROVERS_REGISTRY = { diff --git a/vulnerabilities/pipelines/compute_package_version.py b/vulnerabilities/pipelines/compute_package_version_rank.py similarity index 100% rename from vulnerabilities/pipelines/compute_package_version.py rename to vulnerabilities/pipelines/compute_package_version_rank.py diff --git a/vulnerabilities/tests/test_compute_package_version_rank.py b/vulnerabilities/tests/test_compute_package_version_rank.py new file mode 100644 index 000000000..12cd172a8 --- /dev/null +++ b/vulnerabilities/tests/test_compute_package_version_rank.py @@ -0,0 +1,59 @@ +from unittest.mock import patch + +import pytest +from univers.versions import Version + +from vulnerabilities.models import Package +from vulnerabilities.pipelines.compute_package_version_rank import ComputeVersionRankPipeline + + +@pytest.mark.django_db +class TestComputeVersionRankPipeline: + @pytest.fixture + def pipeline(self): + return ComputeVersionRankPipeline() + + @pytest.fixture + def packages(self, db): + package_type = "pypi" + namespace = "test_namespace" + name = "test_package" + Package.objects.create(type=package_type, namespace=namespace, name=name, version="1.0.0") + Package.objects.create(type=package_type, namespace=namespace, name=name, version="1.1.0") + Package.objects.create(type=package_type, namespace=namespace, name=name, version="0.9.0") + return Package.objects.filter(type=package_type, namespace=namespace, name=name) + + def test_compute_and_store_version_rank(self, pipeline, packages): + with patch.object(pipeline, "log") as mock_log: + pipeline.compute_and_store_version_rank() + assert mock_log.call_count > 0 + for package in packages: + assert package.version_rank is not None + + def test_update_version_rank_for_group(self, pipeline, packages): + with patch.object(Package.objects, "bulk_update") as mock_bulk_update: + pipeline.update_version_rank_for_group(packages) + mock_bulk_update.assert_called_once() + updated_packages = mock_bulk_update.call_args[0][0] + assert len(updated_packages) == len(packages) + for idx, package in enumerate(sorted(packages, key=lambda p: Version(p.version))): + assert updated_packages[idx].version_rank == idx + + def test_sort_packages_by_version(self, pipeline, packages): + sorted_packages = pipeline.sort_packages_by_version(packages) + versions = [p.version for p in sorted_packages] + assert versions == sorted(versions, key=Version) + + def test_sort_packages_by_version_empty(self, pipeline): + assert pipeline.sort_packages_by_version([]) == [] + + def test_sort_packages_by_version_invalid_scheme(self, pipeline, packages): + for package in packages: + package.type = "invalid" + assert pipeline.sort_packages_by_version(packages) == [] + + def test_compute_and_store_version_rank_invalid_scheme(self, pipeline): + Package.objects.create(type="invalid", namespace="test", name="package", version="1.0.0") + with patch.object(pipeline, "log") as mock_log: + pipeline.compute_and_store_version_rank() + mock_log.assert_any_call("Successfully populated `version_rank` for all packages.") From 4e7746ad9909787117f9390e31552039539955d2 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 5 Dec 2024 15:56:27 +0530 Subject: [PATCH 03/10] Add calculate_version_rank on Package Signed-off-by: Tushar Goel --- .../0085_alter_package_version_rank.py | 21 +++++ vulnerabilities/models.py | 93 +++++++++++++++---- vulnerabilities/tests/test_api.py | 9 +- vulnerabilities/tests/test_models.py | 7 +- 4 files changed, 106 insertions(+), 24 deletions(-) create mode 100644 vulnerabilities/migrations/0085_alter_package_version_rank.py diff --git a/vulnerabilities/migrations/0085_alter_package_version_rank.py b/vulnerabilities/migrations/0085_alter_package_version_rank.py new file mode 100644 index 000000000..88f89d82f --- /dev/null +++ b/vulnerabilities/migrations/0085_alter_package_version_rank.py @@ -0,0 +1,21 @@ +# Generated by Django 4.2.16 on 2024-12-05 09:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("vulnerabilities", "0084_alter_package_options_package_version_rank"), + ] + + operations = [ + migrations.AlterField( + model_name="package", + name="version_rank", + field=models.FloatField( + default=0, + help_text="Rank of the version to support ordering by version. Rank zero means the rank has not been defined yet", + ), + ), + ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index ba265a957..54dd7c5bc 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -705,7 +705,7 @@ class Package(PackageURLMixin): "indicate greater vulnerability risk for the package.", ) - version_rank = models.IntegerField( + version_rank = models.FloatField( help_text="Rank of the version to support ordering by version. Rank " "zero means the rank has not been defined yet", default=0, @@ -749,6 +749,65 @@ class Meta: def __str__(self): return self.package_url + @property + def calculate_version_rank(self): + """ + Calculate and return the `version_rank` for a package that does not have one. + If this package already has a `version_rank`, return it. + + The calculated rank will be interpolated between two packages that have + `version_rank` values and are closest to this package in terms of version order. + """ + + if self.version_rank > 0: + return self.version_rank + + # Determine the version_class for this package's type + version_class = RANGE_CLASS_BY_SCHEMES.get(self.type).version_class + if not version_class: + raise ValueError(f"No version_class defined for package type {self.type}") + + group_packages = Package.objects.filter( + type=self.type, + namespace=self.namespace, + name=self.name, + ) + + # if all packages have version rank 0 + + if all(p.version_rank == 0 for p in group_packages): + sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) + for rank, package in enumerate(sorted_packages): + package.version_rank = rank + Package.objects.bulk_update(sorted_packages, fields=["version_rank"]) + return self.version_rank + + group_packages = group_packages.exclude(version_rank=0) + sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) + current_version = version_class(self.version) + + lower_package, higher_package = None, None + for package in sorted_packages: + package_version = version_class(package.version) + if package_version < current_version: + lower_package = package + elif package_version > current_version: + higher_package = package + break + + if lower_package and higher_package: + # Interpolate rank between neighbors + return (lower_package.version_rank + higher_package.version_rank) / 2 + elif lower_package: + # If only lower neighbor exists, assign a rank slightly higher than the lower neighbor + return lower_package.version_rank + 1 + elif higher_package: + # If only higher neighbor exists, assign a rank slightly lower than the higher neighbor + return higher_package.version_rank - 1 + else: + # No neighbors with version_rank; return default rank (e.g., 0) + return 0 + @property def affected_by(self): """ @@ -795,14 +854,6 @@ def get_details_url(self, request): return reverse("package_details", kwargs={"purl": self.purl}, request=request) - def sort_by_version(self, packages): - """ - Return a sequence of `packages` sorted by version. - """ - if not packages: - return [] - return sorted(packages, key=lambda x: self.version_class(x.version)) - @cached_property def version_class(self): range_class = RANGE_CLASS_BY_SCHEMES.get(self.type) @@ -837,19 +888,21 @@ def get_non_vulnerable_versions(self): Return a tuple of the next and latest non-vulnerable versions as Package instance. Return a tuple of (None, None) if there is no non-vulnerable version. """ + if self.version_rank == 0: + self.calculate_version_rank non_vulnerable_versions = Package.objects.get_fixed_by_package_versions( self, fix=False ).only_non_vulnerable() - sorted_versions = self.sort_by_version(non_vulnerable_versions) + sorted_versions = non_vulnerable_versions - later_non_vulnerable_versions = [ - non_vuln_ver - for non_vuln_ver in sorted_versions - if self.version_class(non_vuln_ver.version) > self.current_version - ] + later_non_vulnerable_versions = non_vulnerable_versions.filter( + version_rank__gt=self.version_rank + ) + + later_non_vulnerable_versions = list(later_non_vulnerable_versions) if later_non_vulnerable_versions: - sorted_versions = self.sort_by_version(later_non_vulnerable_versions) + sorted_versions = later_non_vulnerable_versions next_non_vulnerable = sorted_versions[0] latest_non_vulnerable = sorted_versions[-1] return next_non_vulnerable, latest_non_vulnerable @@ -878,6 +931,8 @@ def get_affecting_vulnerabilities(self): Return a list of vulnerabilities that affect this package together with information regarding the versions that fix the vulnerabilities. """ + if self.version_rank == 0: + self.calculate_version_rank package_details_vulns = [] fixed_by_packages = Package.objects.get_fixed_by_package_versions(self, fix=True) @@ -901,12 +956,13 @@ def get_affecting_vulnerabilities(self): if fixed_version > self.current_version: later_fixed_packages.append(fixed_pkg) - next_fixed_package = None next_fixed_package_vulns = [] sort_fixed_by_packages_by_version = [] if later_fixed_packages: - sort_fixed_by_packages_by_version = self.sort_by_version(later_fixed_packages) + sort_fixed_by_packages_by_version = sorted( + later_fixed_packages, key=lambda p: p.version_rank + ) fixed_by_pkgs = [] @@ -936,6 +992,7 @@ def fixing_vulnerabilities(self): """ Return a queryset of Vulnerabilities that are fixed by this package. """ + print("A") return self.fixed_by_vulnerabilities.all() @property diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py index 14a361ecf..41590f590 100644 --- a/vulnerabilities/tests/test_api.py +++ b/vulnerabilities/tests/test_api.py @@ -489,6 +489,7 @@ def setUp(self): self.pkg_2_14_0_rc1 = from_purl( "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1" ) + self.pkg_2_12_6.calculate_version_rank set_as_fixing(package=self.pkg_2_12_6, vulnerability=self.vul3) @@ -526,14 +527,14 @@ def test_api_packages_single_with_purl_in_query_num_queries(self): self.csrf_client.get(f"/api/packages/?purl={self.pkg_2_14_0_rc1.purl}", format="json") def test_api_packages_single_with_purl_no_version_in_query_num_queries(self): - with self.assertNumQueries(64): + with self.assertNumQueries(68): self.csrf_client.get( f"/api/packages/?purl=pkg:maven/com.fasterxml.jackson.core/jackson-databind", format="json", ) def test_api_packages_bulk_search(self): - with self.assertNumQueries(45): + with self.assertNumQueries(49): packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1] purls = [p.purl for p in packages] @@ -546,7 +547,7 @@ def test_api_packages_bulk_search(self): ).json() def test_api_packages_with_lookup(self): - with self.assertNumQueries(14): + with self.assertNumQueries(18): data = {"purl": self.pkg_2_12_6.purl} resp = self.csrf_client.post( @@ -556,7 +557,7 @@ def test_api_packages_with_lookup(self): ).json() def test_api_packages_bulk_lookup(self): - with self.assertNumQueries(45): + with self.assertNumQueries(49): packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1] purls = [p.purl for p in packages] diff --git a/vulnerabilities/tests/test_models.py b/vulnerabilities/tests/test_models.py index 78da37b9d..014754786 100644 --- a/vulnerabilities/tests/test_models.py +++ b/vulnerabilities/tests/test_models.py @@ -423,8 +423,11 @@ def test_sort_by_version(self): version="3.0.0", ) - sorted_pkgs = requesting_package.sort_by_version(vuln_pkg_list) - first_sorted_item = sorted_pkgs[0] + requesting_package.calculate_version_rank + + sorted_pkgs = Package.objects.filter(package_url__in=list_to_sort) + + sorted_pkgs = list(sorted_pkgs) assert sorted_pkgs[0].purl == "pkg:npm/sequelize@3.9.1" assert sorted_pkgs[-1].purl == "pkg:npm/sequelize@3.40.1" From 8a7460005ec501ec539f8325633c49243361ac6e Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 5 Dec 2024 16:09:27 +0530 Subject: [PATCH 04/10] Start enumerating from 1 Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 2 +- vulnerabilities/pipelines/compute_package_version_rank.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 54dd7c5bc..6aa49a136 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -777,7 +777,7 @@ def calculate_version_rank(self): if all(p.version_rank == 0 for p in group_packages): sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) - for rank, package in enumerate(sorted_packages): + for rank, package in enumerate(sorted_packages, start=1): package.version_rank = rank Package.objects.bulk_update(sorted_packages, fields=["version_rank"]) return self.version_rank diff --git a/vulnerabilities/pipelines/compute_package_version_rank.py b/vulnerabilities/pipelines/compute_package_version_rank.py index fd80999b1..0c3f7f47a 100644 --- a/vulnerabilities/pipelines/compute_package_version_rank.py +++ b/vulnerabilities/pipelines/compute_package_version_rank.py @@ -73,7 +73,7 @@ def update_version_rank_for_group(self, packages): # Assign version ranks updates = [] - for rank, package in enumerate(sorted_packages): + for rank, package in enumerate(sorted_packages, start=1): package.version_rank = rank updates.append(package) From b1d8f0360e4fe3213f7ceab1a862fb335fabdff4 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 5 Dec 2024 16:31:19 +0530 Subject: [PATCH 05/10] Fix tests Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 1 - vulnerabilities/tests/test_api.py | 9 +++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 6aa49a136..121e9e2a5 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -893,7 +893,6 @@ def get_non_vulnerable_versions(self): non_vulnerable_versions = Package.objects.get_fixed_by_package_versions( self, fix=False ).only_non_vulnerable() - sorted_versions = non_vulnerable_versions later_non_vulnerable_versions = non_vulnerable_versions.filter( version_rank__gt=self.version_rank diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py index 41590f590..d2119cb06 100644 --- a/vulnerabilities/tests/test_api.py +++ b/vulnerabilities/tests/test_api.py @@ -527,14 +527,14 @@ def test_api_packages_single_with_purl_in_query_num_queries(self): self.csrf_client.get(f"/api/packages/?purl={self.pkg_2_14_0_rc1.purl}", format="json") def test_api_packages_single_with_purl_no_version_in_query_num_queries(self): - with self.assertNumQueries(68): + with self.assertNumQueries(64): self.csrf_client.get( f"/api/packages/?purl=pkg:maven/com.fasterxml.jackson.core/jackson-databind", format="json", ) def test_api_packages_bulk_search(self): - with self.assertNumQueries(49): + with self.assertNumQueries(45): packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1] purls = [p.purl for p in packages] @@ -547,7 +547,7 @@ def test_api_packages_bulk_search(self): ).json() def test_api_packages_with_lookup(self): - with self.assertNumQueries(18): + with self.assertNumQueries(14): data = {"purl": self.pkg_2_12_6.purl} resp = self.csrf_client.post( @@ -557,7 +557,7 @@ def test_api_packages_with_lookup(self): ).json() def test_api_packages_bulk_lookup(self): - with self.assertNumQueries(49): + with self.assertNumQueries(45): packages = [self.pkg_2_12_6, self.pkg_2_12_6_1, self.pkg_2_13_1] purls = [p.purl for p in packages] @@ -609,6 +609,7 @@ def setUp(self): self.pkg_2_14_0_rc1 = from_purl( "pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.0-rc1" ) + self.pkg_2_12_6.calculate_version_rank self.ref = VulnerabilityReference.objects.create( reference_type="advisory", reference_id="CVE-xxx-xxx", url="https://example.com" From 23bcc98a3ebe1147ac7531527e30527ebf4bb1aa Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Thu, 5 Dec 2024 17:06:29 +0530 Subject: [PATCH 06/10] Q Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 80 ++++++++++++++++++++++++--------------- 1 file changed, 49 insertions(+), 31 deletions(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 121e9e2a5..5a0bcdd50 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -762,51 +762,69 @@ def calculate_version_rank(self): if self.version_rank > 0: return self.version_rank - # Determine the version_class for this package's type - version_class = RANGE_CLASS_BY_SCHEMES.get(self.type).version_class - if not version_class: - raise ValueError(f"No version_class defined for package type {self.type}") - group_packages = Package.objects.filter( type=self.type, namespace=self.namespace, name=self.name, ) - # if all packages have version rank 0 + sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) if all(p.version_rank == 0 for p in group_packages): - sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) for rank, package in enumerate(sorted_packages, start=1): package.version_rank = rank Package.objects.bulk_update(sorted_packages, fields=["version_rank"]) return self.version_rank - group_packages = group_packages.exclude(version_rank=0) + packages_with_zero_vesion_rank = group_packages.filter(version_rank=0) sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) - current_version = version_class(self.version) - - lower_package, higher_package = None, None - for package in sorted_packages: - package_version = version_class(package.version) - if package_version < current_version: - lower_package = package - elif package_version > current_version: - higher_package = package - break - - if lower_package and higher_package: - # Interpolate rank between neighbors - return (lower_package.version_rank + higher_package.version_rank) / 2 - elif lower_package: - # If only lower neighbor exists, assign a rank slightly higher than the lower neighbor - return lower_package.version_rank + 1 - elif higher_package: - # If only higher neighbor exists, assign a rank slightly lower than the higher neighbor - return higher_package.version_rank - 1 - else: - # No neighbors with version_rank; return default rank (e.g., 0) - return 0 + + if not packages_with_zero_vesion_rank: + return self.version_rank + + for package in packages_with_zero_vesion_rank: + # Instead of interpolating the rank between the two closest neighbors, + + # Should not we calculate rank for all packages again, we are using O(n^k) here where k is the number of packages with version_rank = 0 + # If we reassign rank to all packages, we can avoid this issue + # It can be done in O(n) time complexity + # Determine the version_class for this package's type + version_class = RANGE_CLASS_BY_SCHEMES.get(package.type).version_class + if not version_class: + raise ValueError(f"No version_class defined for package type {package.type}") + + current_version = version_class(package.version) + + lower_package, higher_package = None, None + + for package in sorted_packages: + package_version = version_class(package.version) + if package_version < current_version: + lower_package = package + elif package_version > current_version: + higher_package = package + break + + if lower_package and higher_package: + # Interpolate rank between neighbors + package.version_rank = ( + lower_package.version_rank + higher_package.version_rank + ) / 2 + + elif lower_package: + # If only lower neighbor exists, assign a rank slightly higher than the lower neighbor + package.version_rank = lower_package.version_rank + 1 + + elif higher_package: + # If only higher neighbor exists, assign a rank slightly lower than the higher neighbor + package.version_rank = higher_package.version_rank - 1 + + else: + # No neighbors with version_rank; return default rank (e.g., 0) + package.version_rank = 0 + + Package.objects.bulk_update(packages_with_zero_vesion_rank, fields=["version_rank"]) + return self.version_rank @property def affected_by(self): From 5fa7cdec6056667c0213a61d746e849c16f24faa Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Sun, 8 Dec 2024 14:31:43 +0530 Subject: [PATCH 07/10] Use integer instead of loat for version rank Signed-off-by: Tushar Goel --- .../0085_alter_package_version_rank.py | 21 ------- vulnerabilities/models.py | 60 +------------------ 2 files changed, 3 insertions(+), 78 deletions(-) delete mode 100644 vulnerabilities/migrations/0085_alter_package_version_rank.py diff --git a/vulnerabilities/migrations/0085_alter_package_version_rank.py b/vulnerabilities/migrations/0085_alter_package_version_rank.py deleted file mode 100644 index 88f89d82f..000000000 --- a/vulnerabilities/migrations/0085_alter_package_version_rank.py +++ /dev/null @@ -1,21 +0,0 @@ -# Generated by Django 4.2.16 on 2024-12-05 09:37 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ("vulnerabilities", "0084_alter_package_options_package_version_rank"), - ] - - operations = [ - migrations.AlterField( - model_name="package", - name="version_rank", - field=models.FloatField( - default=0, - help_text="Rank of the version to support ordering by version. Rank zero means the rank has not been defined yet", - ), - ), - ] diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 5a0bcdd50..89a916274 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -705,7 +705,7 @@ class Package(PackageURLMixin): "indicate greater vulnerability risk for the package.", ) - version_rank = models.FloatField( + version_rank = models.IntegerField( help_text="Rank of the version to support ordering by version. Rank " "zero means the rank has not been defined yet", default=0, @@ -759,73 +759,19 @@ def calculate_version_rank(self): `version_rank` values and are closest to this package in terms of version order. """ - if self.version_rank > 0: - return self.version_rank - group_packages = Package.objects.filter( type=self.type, namespace=self.namespace, name=self.name, ) - sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) - - if all(p.version_rank == 0 for p in group_packages): + if any(p.version_rank == 0 for p in group_packages): + sorted_packages = sorted(group_packages, key=lambda p: self.version_class(p.version)) for rank, package in enumerate(sorted_packages, start=1): package.version_rank = rank Package.objects.bulk_update(sorted_packages, fields=["version_rank"]) return self.version_rank - packages_with_zero_vesion_rank = group_packages.filter(version_rank=0) - sorted_packages = sorted(group_packages, key=lambda p: version_class(p.version)) - - if not packages_with_zero_vesion_rank: - return self.version_rank - - for package in packages_with_zero_vesion_rank: - # Instead of interpolating the rank between the two closest neighbors, - - # Should not we calculate rank for all packages again, we are using O(n^k) here where k is the number of packages with version_rank = 0 - # If we reassign rank to all packages, we can avoid this issue - # It can be done in O(n) time complexity - # Determine the version_class for this package's type - version_class = RANGE_CLASS_BY_SCHEMES.get(package.type).version_class - if not version_class: - raise ValueError(f"No version_class defined for package type {package.type}") - - current_version = version_class(package.version) - - lower_package, higher_package = None, None - - for package in sorted_packages: - package_version = version_class(package.version) - if package_version < current_version: - lower_package = package - elif package_version > current_version: - higher_package = package - break - - if lower_package and higher_package: - # Interpolate rank between neighbors - package.version_rank = ( - lower_package.version_rank + higher_package.version_rank - ) / 2 - - elif lower_package: - # If only lower neighbor exists, assign a rank slightly higher than the lower neighbor - package.version_rank = lower_package.version_rank + 1 - - elif higher_package: - # If only higher neighbor exists, assign a rank slightly lower than the higher neighbor - package.version_rank = higher_package.version_rank - 1 - - else: - # No neighbors with version_rank; return default rank (e.g., 0) - package.version_rank = 0 - - Package.objects.bulk_update(packages_with_zero_vesion_rank, fields=["version_rank"]) - return self.version_rank - @property def affected_by(self): """ From 8cf101e6d0da034f604dee65141ef03441291403 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Sun, 8 Dec 2024 14:35:42 +0530 Subject: [PATCH 08/10] Return version rank anyhow Signed-off-by: Tushar Goel --- vulnerabilities/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/models.py b/vulnerabilities/models.py index 89a916274..c2e89022f 100644 --- a/vulnerabilities/models.py +++ b/vulnerabilities/models.py @@ -770,7 +770,7 @@ def calculate_version_rank(self): for rank, package in enumerate(sorted_packages, start=1): package.version_rank = rank Package.objects.bulk_update(sorted_packages, fields=["version_rank"]) - return self.version_rank + return self.version_rank @property def affected_by(self): From 188ea1c39a0d62f0b5d73a660d6b871034438738 Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Sun, 8 Dec 2024 14:42:40 +0530 Subject: [PATCH 09/10] Fix API tests Signed-off-by: Tushar Goel --- vulnerabilities/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vulnerabilities/tests/test_api.py b/vulnerabilities/tests/test_api.py index d2119cb06..a5f80aa06 100644 --- a/vulnerabilities/tests/test_api.py +++ b/vulnerabilities/tests/test_api.py @@ -808,7 +808,7 @@ def test_api_with_ghost_package_no_fixing_vulnerabilities(self): "qualifiers": {}, "subpath": "", "is_vulnerable": True, - "next_non_vulnerable_version": "2.14.0-rc1", + "next_non_vulnerable_version": "2.12.6", "latest_non_vulnerable_version": "2.14.0-rc1", "affected_by_vulnerabilities": [ { From c91afce492bf4b05c2d78349a770a3b3abf4641c Mon Sep 17 00:00:00 2001 From: Tushar Goel Date: Sun, 8 Dec 2024 14:44:57 +0530 Subject: [PATCH 10/10] Address review comments Signed-off-by: Tushar Goel --- vulnerabilities/pipelines/compute_package_version_rank.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/vulnerabilities/pipelines/compute_package_version_rank.py b/vulnerabilities/pipelines/compute_package_version_rank.py index 0c3f7f47a..73d4aa60a 100644 --- a/vulnerabilities/pipelines/compute_package_version_rank.py +++ b/vulnerabilities/pipelines/compute_package_version_rank.py @@ -91,9 +91,3 @@ def sort_packages_by_version(self, packages): if not version_class: version_class = Version return sorted(packages, key=lambda p: version_class(p.version)) - - def log(self, message): - """ - Simple logging function. - """ - print(message)