diff --git a/docs/new_source.md b/docs/new_source.md index 7c687ae59e1..f74616455cd 100644 --- a/docs/new_source.md +++ b/docs/new_source.md @@ -21,7 +21,7 @@ The step by step instructions are as follows: - [ ] Prepare and publish your records via a public Git repository ([example](https://github.com/AlmaLinux/osv-database/tree/master)). If this method isn’t ideal, we also support publishing records through [REST API](./rest-api.md) or GCS buckets ([example](https://storage.googleapis.com/android-osv/)). -- [ ] To support API querying, if you are contributing a new ecosystem, please create a PR to extend [purl\_helpers.py](https://github.com/google/osv.dev/blob/master/osv/purl_helpers.py) and create a new ecosystem in [\_ecosystems.py](https://github.com/google/osv.dev/blob/master/osv/ecosystems/_ecosystems.py). You can refer to existing examples showing how to implement support for [Semver](https://github.com/google/osv.dev/blob/139de7b69a2ea39e2113309b3a0a47aab920ddcf/osv/ecosystems/_ecosystems.py#L45) and [non-Semver](https://github.com/google/osv.dev/pull/3430) ecosystems. +- [ ] To support API querying, if you are contributing a new ecosystem, please create a PR to extend [purl\_helpers.py](https://github.com/google/osv.dev/blob/master/osv/purl_helpers.py) and create a new ecosystem in [\_ecosystems.py](https://github.com/google/osv.dev/blob/master/osv/ecosystems/_ecosystems.py). You can refer to existing examples showing how to implement support for Semver and non-Semver ecosystems. - [ ] Create a PR to start [importing the records you are publishing into our test instance of OSV.dev](https://github.com/google/osv.dev/blob/master/source_test.yaml) and validate everything is working as intended there. diff --git a/gcp/api/server.py b/gcp/api/server.py index 428bbffb81b..ac2c98e3a3d 100644 --- a/gcp/api/server.py +++ b/gcp/api/server.py @@ -840,7 +840,7 @@ def do_query(query: osv_service_v1_pb2.Query, if purl.version: version = purl.version - if ecosystem and not ecosystems.get(ecosystem): + if ecosystem and not ecosystems.is_known(ecosystem): context.service_context.abort(grpc.StatusCode.INVALID_ARGUMENT, 'Invalid ecosystem.') @@ -1229,8 +1229,8 @@ def query_by_version( query = query.filter(osv.Bug.ecosystem == ecosystem) ecosystem_info = ecosystems.get(ecosystem) - is_semver = ecosystem_info and ecosystem_info.is_semver - supports_comparing = ecosystem_info and ecosystem_info.supports_comparing + is_semver = ecosystems.is_semver(ecosystem) + supports_comparing = ecosystem_info is not None bugs = [] if ecosystem: diff --git a/gcp/api/server_new.py b/gcp/api/server_new.py index a8b0eb304e7..5adf66db3fb 100644 --- a/gcp/api/server_new.py +++ b/gcp/api/server_new.py @@ -110,8 +110,7 @@ def _match_versions(version: str, affected: osv.AffectedVersions) -> bool: """Check if the given version matches one of the AffectedVersions' listed versions.""" ecosystem_helper = osv.ecosystems.get(affected.ecosystem) - if ecosystem_helper and (ecosystem_helper.supports_comparing or - ecosystem_helper.is_semver): + if ecosystem_helper is not None: # Most ecosystem helpers return a very large version on invalid, but if it # does cause an error, just match nothing. try: @@ -150,8 +149,7 @@ def _match_versions(version: str, affected: osv.AffectedVersions) -> bool: def _match_events(version: str, affected: osv.AffectedVersions) -> bool: """Check if the given version matches in the AffectedVersions' events list.""" ecosystem_helper = osv.ecosystems.get(affected.ecosystem) - if not (ecosystem_helper and - (ecosystem_helper.supports_comparing or ecosystem_helper.is_semver)): + if ecosystem_helper is None: # Ecosystem does not support comparisons. return False try: diff --git a/gcp/workers/worker/worker.py b/gcp/workers/worker/worker.py index 7f2a8590e4b..694e72a1ea5 100644 --- a/gcp/workers/worker/worker.py +++ b/gcp/workers/worker/worker.py @@ -288,18 +288,18 @@ def maybe_normalize_package_names(vulnerability): return vulnerability -def filter_unsupported_ecosystems(vulnerability): - """Remove unsupported ecosystems from vulnerability.""" +def filter_unknown_ecosystems(vulnerability): + """Remove unknown ecosystems from vulnerability.""" filtered = [] for affected in vulnerability.affected: # CVE-converted OSV records have no package information. if not affected.HasField('package'): filtered.append(affected) - elif osv.ecosystems.get(affected.package.ecosystem): + elif osv.ecosystems.is_known(affected.package.ecosystem): filtered.append(affected) else: - logging.warning('%s contains unsupported ecosystem "%s"', - vulnerability.id, affected.package.ecosystem) + logging.error('%s contains unknown ecosystem "%s"', vulnerability.id, + affected.package.ecosystem) del vulnerability.affected[:] vulnerability.affected.extend(filtered) @@ -496,7 +496,7 @@ def _do_update(self, source_repo, repo, vulnerability, relative_path, logging.warning('%s has an encoding error, skipping.', vulnerability.id) return - filter_unsupported_ecosystems(vulnerability) + filter_unknown_ecosystems(vulnerability) orig_modified_date = vulnerability.modified.ToDatetime(datetime.UTC) try: diff --git a/gcp/workers/worker/worker_test.py b/gcp/workers/worker/worker_test.py index de9a5da23d5..4bf1edac91c 100644 --- a/gcp/workers/worker/worker_test.py +++ b/gcp/workers/worker/worker_test.py @@ -834,7 +834,7 @@ def setUp(self): # Add fake ecosystems used in tests to supported ecosystems. osv.ecosystems._ecosystems._ecosystems.update({ - 'ecosystem': osv.ecosystems.OrderingUnsupportedEcosystem(), + 'ecosystem': None, }) def tearDown(self): diff --git a/osv/ecosystems/__init__.py b/osv/ecosystems/__init__.py index ef8a01b8661..83b771ea3c3 100644 --- a/osv/ecosystems/__init__.py +++ b/osv/ecosystems/__init__.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,4 +14,4 @@ """Ecosystem helpers.""" from ._ecosystems import * -from .helper_base import * +from .ecosystems_base import * diff --git a/osv/ecosystems/_ecosystems.py b/osv/ecosystems/_ecosystems.py index 8416174a5bb..1ccc811218f 100644 --- a/osv/ecosystems/_ecosystems.py +++ b/osv/ecosystems/_ecosystems.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,78 +15,79 @@ import re -from osv.ecosystems.chainguard import Chainguard -from osv.ecosystems.wolfi import Wolfi -from .helper_base import Ecosystem, OrderingUnsupportedEcosystem -from .alma_linux import AlmaLinux -from .alpaquita import Alpaquita -from .alpine import Alpine +from .ecosystems_base import EnumerableEcosystem, OrderedEcosystem +from .alpine import Alpine, APK from .bioconductor import Bioconductor from .cran import CRAN -from .debian import Debian -from .echo import Echo +from .debian import Debian, DPKG from .haskell import Hackage, GHC -from .mageia import Mageia from .maven import Maven -from .minimos import MinimOS from .nuget import NuGet -from .openeuler import OpenEuler from .packagist import Packagist from .pub import Pub from .pypi import PyPI -from .rocky_linux import RockyLinux -from .redhat import RedHat +from .redhat import RPM from .rubygems import RubyGems from .semver_ecosystem_helper import SemverEcosystem from .ubuntu import Ubuntu -from .suse import SUSE -from .opensuse import OpenSUSE _ecosystems = { - # SemVer-based ecosystems (remember keep synced with SEMVER_ECOSYSTEMS): - 'Bitnami': SemverEcosystem(), - 'crates.io': SemverEcosystem(), - 'Go': SemverEcosystem(), - 'Hex': SemverEcosystem(), - 'npm': SemverEcosystem(), - 'SwiftURL': SemverEcosystem(), - # Non SemVer-based ecosystems - 'Bioconductor': Bioconductor(), - 'CRAN': CRAN(), - 'Chainguard': Chainguard(), - 'Echo': Echo(), - 'GHC': GHC(), - 'Hackage': Hackage(), - 'Maven': Maven(), - 'MinimOS': MinimOS(), - 'NuGet': NuGet(), - 'Packagist': Packagist(), - 'Pub': Pub(), - 'PyPI': PyPI(), - 'RubyGems': RubyGems(), - 'Wolfi': Wolfi(), - # Ecosystems which require a release version for enumeration, which is - # handled separately in get(). - # Ecosystems missing implementations: - 'Android': OrderingUnsupportedEcosystem(), - 'ConanCenter': OrderingUnsupportedEcosystem(), - 'GitHub Actions': OrderingUnsupportedEcosystem(), - 'Linux': OrderingUnsupportedEcosystem(), - 'OSS-Fuzz': OrderingUnsupportedEcosystem(), - 'Photon OS': OrderingUnsupportedEcosystem(), - 'GIT': OrderingUnsupportedEcosystem(), + 'AlmaLinux': RPM, + 'Alpaquita': APK, + 'Alpine': Alpine, + 'BellSoft Hardened Containers': APK, + 'Bioconductor': Bioconductor, + 'Bitnami': SemverEcosystem, + 'Chainguard': APK, + 'CRAN': CRAN, + 'crates.io': SemverEcosystem, + 'Debian': Debian, + 'Echo': DPKG, + 'GHC': GHC, + 'Go': SemverEcosystem, + 'Hackage': Hackage, + 'Hex': SemverEcosystem, + 'Mageia': RPM, + 'Maven': Maven, + 'MinimOS': APK, + 'npm': SemverEcosystem, + 'NuGet': NuGet, + 'openEuler': RPM, + 'openSUSE': RPM, + 'Packagist': Packagist, + 'Pub': Pub, + 'PyPI': PyPI, + 'Red Hat': RPM, + 'Rocky Linux': RPM, + 'RubyGems': RubyGems, + 'SUSE': RPM, + 'SwiftURL': SemverEcosystem, + 'Ubuntu': Ubuntu, + 'Wolfi': APK, + + # Ecosystems known in the schema, but without implementations. + # Must be kept in sync with osv-schema. + 'Android': None, + 'ConanCenter': None, + 'GIT': None, + 'GitHub Actions': None, + 'Linux': None, + 'OSS-Fuzz': None, + 'Photon OS': None, } -# Semver-based ecosystems, should correspond to _ecosystems above. -# TODO(michaelkedar): Avoid need to keep in sync with above. -SEMVER_ECOSYSTEMS = { - 'Bitnami', - 'crates.io', - 'Go', - 'Hex', - 'npm', - 'SwiftURL', -} + +def is_semver(ecosystem: str) -> bool: + """Returns whether an ecosystem uses 'SEMVER' range types""" + return isinstance(get(ecosystem), SemverEcosystem) + + +def is_known(ecosystem: str) -> bool: + """Returns whether an ecosystem is known to OSV + (even if ordering is not supported).""" + name, _, _ = ecosystem.partition(':') + return name in _ecosystems + package_urls = { 'Android': 'https://android.googlesource.com/', @@ -117,50 +118,13 @@ } -def get(name: str) -> Ecosystem: +def get(name: str) -> OrderedEcosystem | EnumerableEcosystem | None: """Get ecosystem helpers for a given ecosystem.""" - - if name.startswith('Debian'): - return Debian(name.partition(':')[2]) - - if name.startswith('AlmaLinux'): - return AlmaLinux() - - if name.startswith('Alpaquita'): - return Alpaquita() - - if name.startswith('Alpine'): - return Alpine(name.partition(':')[2]) - - if name.startswith('BellSoft Hardened Containers'): - return Alpaquita() - - if name.startswith('Mageia'): - return Mageia() - - if name.startswith('Red Hat'): - return RedHat() - - if name.startswith('Rocky Linux'): - return RockyLinux() - - if name.startswith('Photon OS:'): - # TODO(unassigned) - return OrderingUnsupportedEcosystem() - - if name.startswith('Ubuntu'): - return Ubuntu() - - if name.startswith('openSUSE'): - return OpenSUSE() - - if name.startswith('openEuler'): - return OpenEuler() - - if name.startswith('SUSE'): - return SUSE() - - return _ecosystems.get(normalize(name)) + name, _, suffix = name.partition(':') + ecosys = _ecosystems.get(name) + if ecosys is None: + return None + return ecosys(suffix) def normalize(ecosystem_name: str): @@ -205,7 +169,7 @@ def is_supported_in_deps_dev(ecosystem_name: str) -> bool: return ecosystem_name in _OSV_TO_DEPS_ECOSYSTEMS_MAP -def map_ecosystem_to_deps_dev(ecosystem_name: str) -> str: +def map_ecosystem_to_deps_dev(ecosystem_name: str) -> str | None: return _OSV_TO_DEPS_ECOSYSTEMS_MAP.get(ecosystem_name) diff --git a/osv/ecosystems/_ecosystems_test.py b/osv/ecosystems/_ecosystems_test.py index 2d0073dd6e1..a4bad8bbb4a 100644 --- a/osv/ecosystems/_ecosystems_test.py +++ b/osv/ecosystems/_ecosystems_test.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/osv/ecosystems/alma_linux.py b/osv/ecosystems/alma_linux.py deleted file mode 100644 index 2828e23acbb..00000000000 --- a/osv/ecosystems/alma_linux.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""AlmaLinux ecosystem helper.""" - -from ..third_party.univers.rpm import RpmVersion - -from .helper_base import Ecosystem - - -class AlmaLinux(Ecosystem): - """"AlmaLinux ecosystem""" - - def sort_key(self, version): - return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/alma_linux_test.py b/osv/ecosystems/alma_linux_test.py deleted file mode 100644 index 17b385f6a48..00000000000 --- a/osv/ecosystems/alma_linux_test.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""AlmaLinux ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class AlmaLinuxEcosystemTest(unittest.TestCase): - """Almalinux ecosystem helper tests.""" - - def test_alma_linux(self): - """Test sort key""" - ecosystem = ecosystems.get('AlmaLinux') - self.assertGreater( - ecosystem.sort_key("9.27-15.el8_10"), - ecosystem.sort_key("9.27-13.el8_10")) - self.assertGreater( - ecosystem.sort_key("9.27-15.el8_10"), ecosystem.sort_key("0")) - self.assertGreater( - ecosystem.sort_key("3:2.1.10-1.module_el8.10.0+3858+6ad51f9f"), - ecosystem.sort_key("3:2.1.10-1.module_el8.10.0+3845+87b84552")) - self.assertLess( - ecosystem.sort_key("20230404-117.git2e92a49f.el8_8.alma.1"), - ecosystem.sort_key("20240111-121.gitb3132c18.el8")) - self.assertEqual( - ecosystem.sort_key("20240111-121.gitb3132c18.el8"), - ecosystem.sort_key("20240111-121.gitb3132c18.el8")) diff --git a/osv/ecosystems/alpaquita.py b/osv/ecosystems/alpaquita.py deleted file mode 100644 index 94dbc46ec8b..00000000000 --- a/osv/ecosystems/alpaquita.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Alpaquita ecosystem helper.""" - -from ..third_party.univers.alpine import AlpineLinuxVersion - -from .helper_base import Ecosystem - - -class Alpaquita(Ecosystem): - """ - Alpaquita ecosystem - also used for 'BellSoft Hardened Containers' - """ - - def sort_key(self, version): - if not AlpineLinuxVersion.is_valid(version): - # If version is not valid, it is most likely an invalid input - # version then sort it to the last/largest element - return AlpineLinuxVersion('999999') - return AlpineLinuxVersion(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/alpaquita_test.py b/osv/ecosystems/alpaquita_test.py deleted file mode 100644 index dcc5859b0d6..00000000000 --- a/osv/ecosystems/alpaquita_test.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Alpaquita ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class AlpaquitaEcosystemTest(unittest.TestCase): - """Alpaquita ecosystem helper tests.""" - - def test_alpaquita(self): - """Test sort key""" - ecosystem = ecosystems.get('Alpaquita') - # Should not throw exception - ecosystem.sort_key('1.9.5p2') - ecosystem.sort_key('1.9.5p2-r0') - - self.assertGreater( - ecosystem.sort_key('1.9.5p3'), ecosystem.sort_key('1.9.5p2')) - self.assertGreater( - ecosystem.sort_key('1.9.5p1'), ecosystem.sort_key('1.9.5')) - - self.assertGreater( - ecosystem.sort_key('2.78c-r0'), ecosystem.sort_key('2.78a-r1')) - - self.assertGreater( - ecosystem.sort_key('1.13.2-r0'), ecosystem.sort_key('1.13.2_alpha')) - - # Check invalid version handle. - # According to alpaquita.py, invalid versions are sorted to the end. - # '1-0-0' is considered invalid by AlpineLinuxVersion. - self.assertGreater( - ecosystem.sort_key('1-0-0'), ecosystem.sort_key('1.13.2-r0')) - - self.assertEqual( - ecosystem.sort_key('1.13.2-r0'), ecosystem.sort_key('1.13.2-r0')) diff --git a/osv/ecosystems/alpine.py b/osv/ecosystems/alpine.py index 766a81058aa..c1db54e82f6 100644 --- a/osv/ecosystems/alpine.py +++ b/osv/ecosystems/alpine.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -22,12 +22,24 @@ from ..third_party.univers.alpine import AlpineLinuxVersion from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError +from .ecosystems_base import OrderedEcosystem from .. import repos from ..cache import cached -class Alpine(Ecosystem): +class APK(OrderedEcosystem): + """Alpine Package Keeper ecosystem helper.""" + + def sort_key(self, version): + if not AlpineLinuxVersion.is_valid(version): + # If version is not valid, it is most likely an invalid input + # version then sort it to the last/largest element + return AlpineLinuxVersion('999999') + return AlpineLinuxVersion(version) + + +class Alpine(APK, EnumerableEcosystem): """ Alpine packages ecosystem @@ -39,26 +51,19 @@ class Alpine(Ecosystem): # Use github mirror which supports more bandwidth. _APORTS_GIT_URL = 'https://github.com/alpinelinux/aports.git' _BRANCH_SUFFIX = '-stable' - alpine_release_ver: str _GIT_REPO_PATH = 'version_enum/aports/' # Sometimes (2 or 3 packages) APKBUILD files are a bash script and version # is actually stored in variables. _kver is the common variable name. _PKGVER_ALIASES = ('+pkgver=', '+_kver=') _PKGREL_ALIASES = ('+pkgrel=', '+_krel=') - def __init__(self, alpine_release_ver: str): - self.alpine_release_ver = alpine_release_ver + @property + def alpine_release_ver(self) -> str: + return self.suffix if self.suffix is not None else '' def get_branch_name(self) -> str: return self.alpine_release_ver.lstrip('v') + self._BRANCH_SUFFIX - def sort_key(self, version): - if not AlpineLinuxVersion.is_valid(version): - # If version is not valid, it is most likely an invalid input - # version then sort it to the last/largest element - return AlpineLinuxVersion('999999') - return AlpineLinuxVersion(version) - @staticmethod def _process_git_log(output: str) -> list: """ diff --git a/osv/ecosystems/alpine_test.py b/osv/ecosystems/alpine_test.py index 2f7aedf34ab..eca0347ff2a 100644 --- a/osv/ecosystems/alpine_test.py +++ b/osv/ecosystems/alpine_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,17 +11,85 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Alpine ecosystem helper tests.""" +"""APK / Alpine ecosystem helper tests.""" import os import unittest from unittest import mock +import warnings + +from . import alpine from .. import cache from .. import ecosystems from .. import repos +class APKEcosystemTest(unittest.TestCase): + """APK ecosystem helper tests.""" + + def test_apk(self): + """Test APK ecosystem sort_key behaviour""" + ecosystem = alpine.APK() + + # Alpine + # Should not throw exception + ecosystem.sort_key('1.9.5p2') + ecosystem.sort_key('1.9.5p2-r0') + + self.assertGreater( + ecosystem.sort_key('1.9.5p3'), ecosystem.sort_key('1.9.5p2')) + self.assertGreater( + ecosystem.sort_key('1.9.5p1'), ecosystem.sort_key('1.9.5')) + + self.assertGreater( + ecosystem.sort_key('1.13.2-r0'), ecosystem.sort_key('1.13.2_alpha')) + + # Check invalid version handle + self.assertGreater( + ecosystem.sort_key('1-0-0'), ecosystem.sort_key('1.13.2-r0')) + self.assertEqual( + ecosystem.sort_key('1.13.2-r0'), ecosystem.sort_key('1.13.2-r0')) + + # Chainguard + self.assertGreater( + ecosystem.sort_key('38.52.0-r0'), ecosystem.sort_key('37.52.0-r0')) + self.assertLess(ecosystem.sort_key('453'), ecosystem.sort_key('453-r1')) + self.assertGreater(ecosystem.sort_key('5.4.13-r1'), ecosystem.sort_key('0')) + self.assertGreater( + ecosystem.sort_key('1.4.0-r1'), ecosystem.sort_key('1.4.0-r0')) + self.assertGreater( + ecosystem.sort_key('invalid'), ecosystem.sort_key('1.4.0-r0')) + + # MinimOS + self.assertGreater( + ecosystem.sort_key('38.52.0-r0'), ecosystem.sort_key('37.52.0-r0')) + self.assertLess(ecosystem.sort_key('453'), ecosystem.sort_key('453-r1')) + self.assertGreater(ecosystem.sort_key('5.4.13-r1'), ecosystem.sort_key('0')) + self.assertGreater( + ecosystem.sort_key('1.4.0-r1'), ecosystem.sort_key('1.4.0-r0')) + self.assertGreater( + ecosystem.sort_key('invalid'), ecosystem.sort_key('1.4.0-r0')) + self.assertGreater( + ecosystem.sort_key('13.0.14.5-r1'), ecosystem.sort_key('7.64.3-r2')) + self.assertLess( + ecosystem.sort_key('13.0.14.5-r1'), ecosystem.sort_key('16.6-r0')) + + def test_apk_ecosystems(self): + """Test apk-based ecosystems return an APK ecosystem.""" + ecos = [ + 'Alpine', + 'Alpaquita', + 'BellSoft Hardened Containers', + 'Chainguard', + 'MinimOS', + 'Wolfi', + ] + for ecosystem_name in ecos: + ecosystem = ecosystems.get(ecosystem_name) + self.assertIsInstance(ecosystem, alpine.APK) + + class AlpineEcosystemTest(unittest.TestCase): """Alpine ecosystem helper tests.""" _TEST_DATA_DIR = os.path.join( @@ -40,29 +108,18 @@ def test_alpine(self, ensure_updated_checkout_mock: mock.MagicMock): ecosystem = ecosystems.get('Alpine:v3.16') self.assertEqual(ensure_updated_checkout_mock.call_count, 0) # Tests that next version and version enumeration generally works - self.assertEqual('1.12.2-r1', ecosystem.next_version('nginx', '1.12.2')) - self.assertEqual(ensure_updated_checkout_mock.call_count, 1) - self.assertEqual('1.16.1-r0', ecosystem.next_version('nginx', '1.16.0-r4')) - # Second call should use cache, so call count should not increase - self.assertEqual(ensure_updated_checkout_mock.call_count, 1) - - # Should not throw exception - ecosystem.sort_key('1.9.5p2') - ecosystem.sort_key('1.9.5p2-r0') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.12.2-r1', ecosystem.next_version('nginx', '1.12.2')) + self.assertEqual(ensure_updated_checkout_mock.call_count, 1) + self.assertEqual('1.16.1-r0', + ecosystem.next_version('nginx', '1.16.0-r4')) + # Second call should use cache, so call count should not increase + self.assertEqual(ensure_updated_checkout_mock.call_count, 1) - self.assertGreater( - ecosystem.sort_key('1.9.5p3'), ecosystem.sort_key('1.9.5p2')) - self.assertGreater( - ecosystem.sort_key('1.9.5p1'), ecosystem.sort_key('1.9.5')) - - # Check letter suffixes clone correctly - self.assertEqual('2.78c-r0', ecosystem.next_version('blender', '2.78a-r1')) - - self.assertGreater( - ecosystem.sort_key('1.13.2-r0'), ecosystem.sort_key('1.13.2_alpha')) - - # Check invalid version handle - self.assertGreater( - ecosystem.sort_key('1-0-0'), ecosystem.sort_key('1.13.2-r0')) + # Check letter suffixes clone correctly + self.assertEqual('2.78c-r0', + ecosystem.next_version('blender', '2.78a-r1')) ecosystems.config.set_cache(None) diff --git a/osv/ecosystems/bioconductor.py b/osv/ecosystems/bioconductor.py index 4009c5a4307..e56e4a770c4 100644 --- a/osv/ecosystems/bioconductor.py +++ b/osv/ecosystems/bioconductor.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -16,11 +16,11 @@ import requests from . import config -from .helper_base import Ecosystem, EnumerateError -from .. import semver_index +from .ecosystems_base import EnumerableEcosystem, EnumerateError +from .semver_ecosystem_helper import SemverLike -class Bioconductor(Ecosystem): +class Bioconductor(EnumerableEcosystem, SemverLike): """Bioconductor ecosystem helpers.""" # Use the Posit Public Package Manager API to pull both the current and @@ -42,15 +42,6 @@ def get_bioc_versions(self): return [bioc['bioc_version'] for bioc in data['bioc_versions']] - def sort_key(self, version): - """Sort key.""" - try: - return semver_index.parse(version) - except ValueError: - # If a user gives us an unparsable semver version, - # treat it as a very large version so as to not match anything. - return semver_index.parse('999999') - def _enumerate_versions(self, url, bioc_versions, diff --git a/osv/ecosystems/bioconductor_test.py b/osv/ecosystems/bioconductor_test.py index 888ad92a6c7..83594eaf295 100644 --- a/osv/ecosystems/bioconductor_test.py +++ b/osv/ecosystems/bioconductor_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +13,7 @@ # limitations under the License. """Bioconductor ecosystem helper tests.""" +import warnings import vcr.unittest from .. import ecosystems @@ -24,7 +25,10 @@ class BioconductorEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('Bioconductor') - self.assertEqual('1.18.0', ecosystem.next_version('a4', '1.16.0')) - self.assertEqual('1.20.0', ecosystem.next_version('a4', '1.18.0')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.18.0', ecosystem.next_version('a4', '1.16.0')) + self.assertEqual('1.20.0', ecosystem.next_version('a4', '1.18.0')) + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('doesnotexist123456', '1') diff --git a/osv/ecosystems/chainguard.py b/osv/ecosystems/chainguard.py deleted file mode 100644 index ab8a5150d1e..00000000000 --- a/osv/ecosystems/chainguard.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Chainguard ecosystem helper.""" - -from osv.ecosystems.helper_base import Ecosystem -from ..third_party.univers.alpine import AlpineLinuxVersion - - -class Chainguard(Ecosystem): - """Chainguard packages ecosystem""" - - def sort_key(self, version): - # Chainguard uses `apk` package format - if not AlpineLinuxVersion.is_valid(version): - # If version is not valid, it is most likely an invalid input - # version then sort it to the last/largest element - return AlpineLinuxVersion('999999') - return AlpineLinuxVersion(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/chainguard_test.py b/osv/ecosystems/chainguard_test.py deleted file mode 100644 index cb7a530a65a..00000000000 --- a/osv/ecosystems/chainguard_test.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Chainguard ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class ChainguardEcosystemTest(unittest.TestCase): - """Chainguard ecosystem helper tests.""" - - def test_chainguard(self): - """Test sort_key""" - ecosystem = ecosystems.get('Chainguard') - self.assertGreater( - ecosystem.sort_key('38.52.0-r0'), ecosystem.sort_key('37.52.0-r0')) - self.assertLess(ecosystem.sort_key('453'), ecosystem.sort_key('453-r1')) - self.assertGreater(ecosystem.sort_key('5.4.13-r1'), ecosystem.sort_key('0')) - self.assertGreater( - ecosystem.sort_key('1.4.0-r1'), ecosystem.sort_key('1.4.0-r0')) - self.assertGreater( - ecosystem.sort_key('invalid'), ecosystem.sort_key('1.4.0-r0')) diff --git a/osv/ecosystems/config.py b/osv/ecosystems/config.py index c1da276b1fd..46bcfbb5afd 100644 --- a/osv/ecosystems/config.py +++ b/osv/ecosystems/config.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/osv/ecosystems/cran.py b/osv/ecosystems/cran.py index 62f2d21a7bb..00251a4d4c4 100644 --- a/osv/ecosystems/cran.py +++ b/osv/ecosystems/cran.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,10 +17,10 @@ import packaging_legacy.version from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError -class CRAN(Ecosystem): +class CRAN(EnumerableEcosystem): """CRAN ecosystem helpers.""" # Use the Posit Public Package Manager API to pull both the current diff --git a/osv/ecosystems/cran_test.py b/osv/ecosystems/cran_test.py index 97a83e0eaa1..c05a213e257 100644 --- a/osv/ecosystems/cran_test.py +++ b/osv/ecosystems/cran_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -13,6 +13,8 @@ # limitations under the License. """CRAN ecosystem helper tests.""" +import warnings + import vcr.unittest from .. import ecosystems @@ -24,16 +26,20 @@ class CRANEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('CRAN') - # Test typical semver X.Y.Z version - self.assertEqual('0.1.1', ecosystem.next_version('readxl', '0.1.0')) - self.assertEqual('1.0.0', ecosystem.next_version('readxl', '0.1.1')) + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + + # Test typical semver X.Y.Z version + self.assertEqual('0.1.1', ecosystem.next_version('readxl', '0.1.0')) + self.assertEqual('1.0.0', ecosystem.next_version('readxl', '0.1.1')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('doesnotexist123456', '1') + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('doesnotexist123456', '1') - # Test versions with the X.Y-Z format - self.assertEqual('0.1-18', ecosystem.next_version('abd', '0.1-12')) - self.assertEqual('0.2-2', ecosystem.next_version('abd', '0.1-22')) + # Test versions with the X.Y-Z format + self.assertEqual('0.1-18', ecosystem.next_version('abd', '0.1-12')) + self.assertEqual('0.2-2', ecosystem.next_version('abd', '0.1-22')) - # Test atypical versioned package - self.assertEqual('0.99-8.47', ecosystem.next_version('aqp', '0.99-8.1')) + # Test atypical versioned package + self.assertEqual('0.99-8.47', ecosystem.next_version('aqp', '0.99-8.1')) diff --git a/osv/ecosystems/debian.py b/osv/ecosystems/debian.py index 0dae3bbabf1..fcb43e97015 100644 --- a/osv/ecosystems/debian.py +++ b/osv/ecosystems/debian.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -20,10 +20,23 @@ from ..third_party.univers.debian import Version as DebianVersion from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError +from .ecosystems_base import OrderedEcosystem from .. import cache from ..request_helper import RequestError, RequestHelper + +class DPKG(OrderedEcosystem): + """Debian package (dpkg) ecosystem""" + + def sort_key(self, version): + if not DebianVersion.is_valid(version): + # If debian version is not valid, it is most likely an invalid fixed + # version then sort it to the last/largest element + return DebianVersion(999999, '999999') + return DebianVersion.from_string(version) + + # TODO(another-rex): Update this to use dynamically # change depending on the project CLOUD_API_CACHE_URL_TEMPLATE = ( @@ -73,21 +86,14 @@ def get_first_package_version(package_name: str, release_number: str) -> str: return '0' -class Debian(Ecosystem): +class Debian(EnumerableEcosystem, DPKG): """Debian ecosystem""" _API_PACKAGE_URL = 'https://snapshot.debian.org/mr/package/{package}/' - debian_release_ver: str - def __init__(self, debian_release_ver: str): - self.debian_release_ver = debian_release_ver - - def sort_key(self, version): - if not DebianVersion.is_valid(version): - # If debian version is not valid, it is most likely an invalid fixed - # version then sort it to the last/largest element - return DebianVersion(999999, '999999') - return DebianVersion.from_string(version) + @property + def debian_release_ver(self) -> str: + return self.suffix if self.suffix is not None else '' def enumerate_versions(self, package, @@ -137,7 +143,3 @@ def version_is_valid(v): return self._get_affected_versions(versions, introduced, fixed, last_affected, limits) - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/debian_test.py b/osv/ecosystems/debian_test.py index 4dc7cd822e3..ccb491b6af7 100644 --- a/osv/ecosystems/debian_test.py +++ b/osv/ecosystems/debian_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,16 +11,78 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Debian ecosystem helper tests.""" +"""dpkg / Debian ecosystem helper tests.""" import requests import vcr.unittest +import unittest from unittest import mock +import warnings + +from . import debian from .. import cache from .. import ecosystems +class DPKGEcosystemTest(unittest.TestCase): + """dpkg ecosystem helper tests.""" + + def test_dpkg(self): + """Test sort_key""" + ecosystem = debian.DPKG() + # Debian + self.assertGreater( + ecosystem.sort_key('1.13.6-2'), ecosystem.sort_key('1.13.6-1')) + + # Test that specifically is greater than normal versions + self.assertGreater( + ecosystem.sort_key(''), ecosystem.sort_key('1.13.6-1')) + + # Compares base versions + self.assertGreater(ecosystem.sort_key('1.2.3'), ecosystem.sort_key('1.2')) + self.assertGreater(ecosystem.sort_key('1.3'), ecosystem.sort_key('1.2-3')) + + # Compares versions within the same Debian release + self.assertGreater( + ecosystem.sort_key('1.3+deb11u1'), ecosystem.sort_key('1.2+deb11u5')) + + # Compare versions across Debian releases + self.assertGreater( + ecosystem.sort_key('1.2+deb12u1'), + ecosystem.sort_key('1.2+deb11u2')) # deb12 > deb11 + self.assertGreater( + ecosystem.sort_key('1.18+deb11u3'), + ecosystem.sort_key('1.14+deb10u3')) # 1.18 > 1.14 + self.assertGreater( + ecosystem.sort_key('1.18+deb10'), + ecosystem.sort_key('1.14+deb11')) # 1.18 > 1.14 + + # Echo + self.assertGreater( + ecosystem.sort_key("38.52.0-e1"), ecosystem.sort_key("37.52.0-e0")) + self.assertLess(ecosystem.sort_key("453"), ecosystem.sort_key("453-e1")) + self.assertGreater(ecosystem.sort_key("5.4.13-e1"), ecosystem.sort_key("0")) + self.assertGreater( + ecosystem.sort_key("1.4.0-e1"), ecosystem.sort_key("1.4.0-e0")) + self.assertGreater( + ecosystem.sort_key("invalid"), ecosystem.sort_key("1.4.0-e0")) + self.assertGreater( + ecosystem.sort_key("13.0.14.5-e1"), ecosystem.sort_key("7.64.3-e2")) + self.assertLess( + ecosystem.sort_key("13.0.14.5-e1"), ecosystem.sort_key("16.6-e0")) + + def test_dpkg_ecosystems(self): + """Test dpkg-based ecosystems return a DPKG ecosystem.""" + ecos = [ + 'Debian', + 'Echo', + ] + for ecosystem_name in ecos: + ecosystem = ecosystems.get(ecosystem_name) + self.assertIsInstance(ecosystem, debian.DPKG) + + class DebianEcosystemTest(vcr.unittest.VCRTestCase): """Debian ecosystem helper tests.""" @@ -37,18 +99,13 @@ def test_debian(self, first_ver_requests_mock: mock.MagicMock, ecosystem = ecosystems.get('Debian:9') # Tests that next version and version enumeration generally works - self.assertEqual('1.13.6-1', ecosystem.next_version('nginx', '1.13.5-1')) - self.assertEqual('1.13.6-2', ecosystem.next_version('nginx', '1.13.6-1')) - self.assertEqual('3.0.1+dfsg-2', - ecosystem.next_version('blender', '3.0.1+dfsg-1')) - - # Tests that sort key works - self.assertGreater( - ecosystem.sort_key('1.13.6-2'), ecosystem.sort_key('1.13.6-1')) - - # Test that specifically is greater than normal versions - self.assertGreater( - ecosystem.sort_key(''), ecosystem.sort_key('1.13.6-1')) + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.13.6-1', ecosystem.next_version('nginx', '1.13.5-1')) + self.assertEqual('1.13.6-2', ecosystem.next_version('nginx', '1.13.6-1')) + self.assertEqual('3.0.1+dfsg-2', + ecosystem.next_version('blender', '3.0.1+dfsg-1')) # Test that end-of-life enumeration is disabled with self.assertLogs(level='WARNING') as logs: @@ -79,7 +136,9 @@ def test_debian(self, first_ver_requests_mock: mock.MagicMock, self.assertNotIn('2.1.27~101-g0780600+dfsg-3+deb9u1', versions) self.assertNotIn('2.1.27~101-g0780600+dfsg-3+deb9u2', versions) - with self.assertRaises(ecosystems.EnumerateError): + with self.assertRaises( + ecosystems.EnumerateError), warnings.catch_warnings(): + warnings.filterwarnings('ignore', 'Avoid using this method') ecosystem.next_version('doesnotexist123456', '1') self.assertEqual(general_requests_mock.call_count, 5) @@ -90,35 +149,16 @@ def test_debian(self, first_ver_requests_mock: mock.MagicMock, self.assertEqual(general_requests_mock.call_count, 5) ecosystems.config.set_cache(None) - def test_debian_sort_key(self): - """Tests Debian sort key across different releases.""" - ecosystem = ecosystems.get('Debian') - - # Compares base versions - self.assertGreater(ecosystem.sort_key('1.2.3'), ecosystem.sort_key('1.2')) - self.assertGreater(ecosystem.sort_key('1.3'), ecosystem.sort_key('1.2-3')) - - # Compares versions within the same Debian release - self.assertGreater( - ecosystem.sort_key('1.3+deb11u1'), ecosystem.sort_key('1.2+deb11u5')) - - # Compare versions across Debian releases - self.assertGreater( - ecosystem.sort_key('1.2+deb12u1'), - ecosystem.sort_key('1.2+deb11u2')) # deb12 > deb11 - self.assertGreater( - ecosystem.sort_key('1.18+deb11u3'), - ecosystem.sort_key('1.14+deb10u3')) # 1.18 > 1.14 - self.assertGreater( - ecosystem.sort_key('1.18+deb10'), - ecosystem.sort_key('1.14+deb11')) # 1.18 > 1.14 - @mock.patch('osv.cache.Cache') def test_cache(self, cache_mock: mock.MagicMock): + """Test caching works.""" cache_mock.get.return_value = None ecosystems.config.set_cache(cache_mock) - debian = ecosystems.get('Debian:9') - debian.next_version('nginx', '1.13.5-1') + deb = ecosystems.get('Debian:9') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + deb.next_version('nginx', '1.13.5-1') cache_mock.get.assert_called_once() cache_mock.set.assert_called_once() diff --git a/osv/ecosystems/echo.py b/osv/ecosystems/echo.py deleted file mode 100644 index 59e4f0bdd7f..00000000000 --- a/osv/ecosystems/echo.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Echo ecosystem helper.""" - -from ..third_party.univers.debian import Version as DebianVersion - -from .helper_base import Ecosystem - - -class Echo(Ecosystem): - """Echo packages ecosystem""" - - def sort_key(self, version): - # Echo uses `dpkg` package format - if not DebianVersion.is_valid(version): - # If version is not valid, it is most likely an invalid input - # version then sort it to the last/largest element - return DebianVersion(999999, "999999") - return DebianVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError("Ecosystem helper does not support enumeration") - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/echo_test.py b/osv/ecosystems/echo_test.py deleted file mode 100644 index b8ffefd3f8f..00000000000 --- a/osv/ecosystems/echo_test.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Echo ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class EchoEcosystemTest(unittest.TestCase): - """Echo ecosystem helper tests.""" - - def test_echo(self): - """Test sort_key""" - ecosystem = ecosystems.get("Echo") - self.assertGreater( - ecosystem.sort_key("38.52.0-e1"), ecosystem.sort_key("37.52.0-e0")) - self.assertLess(ecosystem.sort_key("453"), ecosystem.sort_key("453-e1")) - self.assertGreater(ecosystem.sort_key("5.4.13-e1"), ecosystem.sort_key("0")) - self.assertGreater( - ecosystem.sort_key("1.4.0-e1"), ecosystem.sort_key("1.4.0-e0")) - self.assertGreater( - ecosystem.sort_key("invalid"), ecosystem.sort_key("1.4.0-e0")) - self.assertGreater( - ecosystem.sort_key("13.0.14.5-e1"), ecosystem.sort_key("7.64.3-e2")) - self.assertLess( - ecosystem.sort_key("13.0.14.5-e1"), ecosystem.sort_key("16.6-e0")) diff --git a/osv/ecosystems/helper_base.py b/osv/ecosystems/ecosystems_base.py similarity index 66% rename from osv/ecosystems/helper_base.py rename to osv/ecosystems/ecosystems_base.py index 3ac11ba2092..0b4cedd345d 100644 --- a/osv/ecosystems/helper_base.py +++ b/osv/ecosystems/ecosystems_base.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,30 +11,57 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Ecosystem helpers base classes.""" - +"""Ecosystems base classes.""" from abc import ABC, abstractmethod -import bisect from typing import Any +from warnings import deprecated +import bisect import requests from urllib.parse import quote from . import config +class OrderedEcosystem(ABC): + """Ecosystem helper that supports comparison between versions.""" + + def __init__(self, suffix: str | None = None): + """init method for all ecosystem helpers. + + `suffix` is optionally used on ecosystems that use them. + e.g. Alpine:v3.16 would use suffix='v3.16' + """ + self.suffix = suffix + + @abstractmethod + def sort_key(self, version: str) -> Any: + """Comparable key for a version. + + If the version string is invalid, return a very large version. + """ + + def sort_versions(self, versions: list[str]): + """Sort versions.""" + versions.sort(key=self.sort_key) + + class EnumerateError(Exception): """Non-retryable version enumeration error.""" -class Ecosystem(ABC): - """Ecosystem helpers.""" - - @property - def name(self): - """Get the name of the ecosystem.""" - return self.__class__.__name__ +class EnumerableEcosystem(OrderedEcosystem, ABC): + """Ecosystem helper that supports version enumeration.""" - def _before_limits(self, version, limits): + @abstractmethod + def enumerate_versions(self, + package: str, + introduced: str | None, + fixed: str | None = None, + last_affected: str | None = None, + limits: list[str] | None = None) -> list[str]: + """Enumerate known versions of a package in a given version range.""" + + def _before_limits(self, version: str, limits: list[str] | None) -> bool: """Return whether the given version is before any limits.""" if not limits or '*' in limits: return True @@ -42,40 +69,9 @@ def _before_limits(self, version, limits): return any( self.sort_key(version) < self.sort_key(limit) for limit in limits) - def next_version(self, package, version): - """Get the next version after the given version.""" - versions = self.enumerate_versions(package, version, fixed=None) - # Check if the key used for sorting is equal as sometimes different - # strings could evaluate to the same version. - if versions and self.sort_key(versions[0]) != self.sort_key(version): - # Version does not exist, so use the first one that would sort - # after it (which is what enumerate_versions returns). - return versions[0] - - if len(versions) > 1: - return versions[1] - - return None - - @abstractmethod - def sort_key(self, version: str) -> Any: - """Sort key.""" - - def sort_versions(self, versions): - """Sort versions.""" - versions.sort(key=self.sort_key) - - @abstractmethod - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - """Enumerate versions.""" - - def _get_affected_versions(self, versions, introduced, fixed, last_affected, - limits): + def _get_affected_versions(self, versions: list[str], introduced: str | None, + fixed: str | None, last_affected: str | None, + limits: list[str] | None) -> list[str]: """Get affected versions. Args: @@ -111,50 +107,34 @@ def _get_affected_versions(self, versions, introduced, fixed, last_affected, affected = versions[start_idx:end_idx] return [v for v in affected if self._before_limits(v, limits)] - @property - def is_semver(self): - return False - - @property - def supports_ordering(self): - return True - - @property - def supports_comparing(self): - """Determines whether to use affected version range comparison - for API queries.""" - return False - - -class OrderingUnsupportedEcosystem(Ecosystem): - """Placeholder ecosystem helper for unimplemented ecosystems.""" - - def sort_key(self, version): - raise NotImplementedError('Ecosystem helper does not support sorting') + @deprecated('Avoid using this method. ' + 'It is provided only to maintain existing tooling.') + def next_version(self, package: str, version: str) -> str | None: + """Get the next version after the given version.""" + versions = self.enumerate_versions(package, version, fixed=None) + # Check if the key used for sorting is equal as sometimes different + # strings could evaluate to the same version. + if versions and self.sort_key(versions[0]) != self.sort_key(version): + # Version does not exist, so use the first one that would sort + # after it (which is what enumerate_versions returns). + return versions[0] - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') + if len(versions) > 1: + return versions[1] - @property - def supports_ordering(self): - return False + return None -class DepsDevMixin(Ecosystem, ABC): +class DepsDevMixin(EnumerableEcosystem, ABC): """deps.dev mixin.""" _DEPS_DEV_PACKAGE_URL = \ 'https://api.deps.dev/v3alpha/systems/{system}/packages/{package}' - _DEPS_DEV_ECOSYSTEM_MAP = { - 'Maven': 'maven', - 'PyPI': 'pypi', - } + @property + @abstractmethod + def deps_dev_system(self) -> str: + """The deps.dev system name.""" def _deps_dev_enumerate(self, package, @@ -163,15 +143,14 @@ def _deps_dev_enumerate(self, last_affected=None, limits=None): """Use deps.dev to get list of versions.""" - ecosystem = self._DEPS_DEV_ECOSYSTEM_MAP[self.name] url = self._DEPS_DEV_PACKAGE_URL.format( - system=ecosystem, package=quote(package, safe='')) + system=self.deps_dev_system, package=quote(package, safe='')) response = requests.get(url, timeout=config.timeout) if response.status_code == 404: raise EnumerateError(f'Package {package} not found') if response.status_code != 200: raise RuntimeError( - f'Failed to get {ecosystem} versions for {package} with: ' + f'Failed to get {self.deps_dev_system} versions for {package} with: ' f'{response.status_code}') response = response.json() versions = [v['versionKey']['version'] for v in response['versions']] diff --git a/osv/ecosystems/ecosystems_test.py b/osv/ecosystems/ecosystems_test.py index ff6a61e0b85..05dc59ba20e 100644 --- a/osv/ecosystems/ecosystems_test.py +++ b/osv/ecosystems/ecosystems_test.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -39,11 +39,6 @@ def test_ecosystem_supported_by_schema(self): self.schema_ecosystems.match(ecosystem), msg=f'"{ecosystem}" not defined in schema') - for ecosystem in _ecosystems.SEMVER_ECOSYSTEMS: - self.assertIsNotNone( - self.schema_ecosystems.match(ecosystem), - msg=f'SEMVER ecosystem "{ecosystem}" not defined in schema') - for ecosystem in _ecosystems.package_urls: self.assertIsNotNone( self.schema_ecosystems.match(ecosystem), diff --git a/osv/ecosystems/haskell.py b/osv/ecosystems/haskell.py index e2a64f57b4b..6baa953ea84 100644 --- a/osv/ecosystems/haskell.py +++ b/osv/ecosystems/haskell.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # Copyright 2023 Fraser Tweedale # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -21,14 +21,13 @@ """ import requests -import typing from . import config -from .helper_base import Ecosystem, EnumerateError -from .. import semver_index +from .ecosystems_base import EnumerableEcosystem, EnumerateError +from .semver_ecosystem_helper import SemverLike -class Hackage(Ecosystem): +class Hackage(EnumerableEcosystem): """Hackage (Haskell package index) ecosystem.""" _API_PACKAGE_URL = 'https://hackage.haskell.org/package/{package}.json' @@ -70,7 +69,7 @@ def enumerate_versions(self, last_affected, limits) -class GHC(Ecosystem): +class GHC(EnumerableEcosystem, SemverLike): """Glasgow Haskell Compiler (GHC) ecosystem.""" _API_PACKAGE_URL = ('https://gitlab.haskell.org' @@ -99,17 +98,9 @@ class GHC(Ecosystem): '7.0.3', '7.0.4-rc1', '7.0.4', ] # yapf: disable - def sort_key(self, version): - """Sort key.""" - try: - return semver_index.parse(version) - except ValueError: - # If a user gives us an unparsable semver version, - # treat it as a very large version so as to not match anything. - return semver_index.parse('999999') @classmethod - def tag_to_version(cls, tag: str) -> typing.Optional[str]: + def tag_to_version(cls, tag: str) -> str | None: """Convert a tag to a release version, or return None if invalid. GHC release tags follow the scheme: diff --git a/osv/ecosystems/haskell_test.py b/osv/ecosystems/haskell_test.py index ca239a98b49..4377409f738 100644 --- a/osv/ecosystems/haskell_test.py +++ b/osv/ecosystems/haskell_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # Copyright 2023 Fraser Tweedale # # Licensed under the Apache License, Version 2.0 (the "License"); @@ -14,6 +14,8 @@ # limitations under the License. """Haskell ecosystem helper tests.""" +import warnings + import vcr.unittest from .. import ecosystems @@ -25,11 +27,14 @@ class HackageEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('Hackage') - self.assertEqual('1.0.0.0', ecosystem.next_version('aeson', '0.11.3.0')) - self.assertEqual('1.0.1.0', ecosystem.next_version('aeson', '1.0.0.0')) - self.assertEqual('0.1.26.0', ecosystem.next_version('jose', '0')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.0.0.0', ecosystem.next_version('aeson', '0.11.3.0')) + self.assertEqual('1.0.1.0', ecosystem.next_version('aeson', '1.0.0.0')) + self.assertEqual('0.1.26.0', ecosystem.next_version('jose', '0')) + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('doesnotexist123456', '1') def test_sort_key(self): """Test sort_key.""" @@ -44,11 +49,14 @@ class GHCEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('GHC') - self.assertEqual('0.29', ecosystem.next_version('GHC', '0')) - self.assertEqual('7.0.4', ecosystem.next_version('GHC', '7.0.4-rc1')) - # 7.0.4 is the last of the hardcoded versions - self.assertEqual('7.2.1', ecosystem.next_version('GHC', '7.0.4')) - - # The whole GHC ecosystem is versioned together. Enumeration ignores - # package/component name. Therefore this should NOT raise: - ecosystem.next_version('doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('0.29', ecosystem.next_version('GHC', '0')) + self.assertEqual('7.0.4', ecosystem.next_version('GHC', '7.0.4-rc1')) + # 7.0.4 is the last of the hardcoded versions + self.assertEqual('7.2.1', ecosystem.next_version('GHC', '7.0.4')) + + # The whole GHC ecosystem is versioned together. Enumeration ignores + # package/component name. Therefore this should NOT raise: + ecosystem.next_version('doesnotexist123456', '1') diff --git a/osv/ecosystems/mageia.py b/osv/ecosystems/mageia.py deleted file mode 100644 index d3b1e4ec4be..00000000000 --- a/osv/ecosystems/mageia.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Mageia ecosystem helper.""" - -from ..third_party.univers.rpm import RpmVersion -from .helper_base import Ecosystem - - -class Mageia(Ecosystem): - """Mageia ecosystem""" - - @property - def name(self): - return 'Mageia' - - def sort_key(self, version): - return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/mageia_test.py b/osv/ecosystems/mageia_test.py deleted file mode 100644 index 38a271d9cbb..00000000000 --- a/osv/ecosystems/mageia_test.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Mageia ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class MageiaEcosystemTest(unittest.TestCase): - """Mageia ecosystem helper tests.""" - - def test_mageia(self): - """Test sort_key""" - ecosystem = ecosystems.get('Mageia') - self.assertEqual('Mageia', ecosystem.name) - self.assertGreater( - ecosystem.sort_key('3.2.7-1.2.mga9'), - ecosystem.sort_key('3.2.7-1.mga9')) - self.assertGreater( - ecosystem.sort_key('3.2.7-1.2.mga9'), ecosystem.sort_key('0')) - self.assertLess(ecosystem.sort_key('invalid'), ecosystem.sort_key('0')) - self.assertGreater( - ecosystem.sort_key('1:1.8.11-1.mga9'), - ecosystem.sort_key('0:1.9.1-2.mga9')) diff --git a/osv/ecosystems/maven.py b/osv/ecosystems/maven.py index 660a20abac3..bcc2d2ed8f7 100644 --- a/osv/ecosystems/maven.py +++ b/osv/ecosystems/maven.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import functools import re -from .helper_base import DepsDevMixin +from .ecosystems_base import EnumerableEcosystem, DepsDevMixin # pylint: disable=line-too-long @@ -233,9 +233,13 @@ def from_string(cls, str_version): return version -class Maven(DepsDevMixin): +class Maven(DepsDevMixin, EnumerableEcosystem): """Maven ecosystem.""" + @property + def deps_dev_system(self) -> str: + return 'maven' + def sort_key(self, version): """Sort key.""" return Version.from_string(version) diff --git a/osv/ecosystems/maven_test.py b/osv/ecosystems/maven_test.py index 4144db5520f..bde3fe99632 100644 --- a/osv/ecosystems/maven_test.py +++ b/osv/ecosystems/maven_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ """Maven ecosystem helper tests.""" import unittest import vcr.unittest +import warnings from . import maven from .. import ecosystems @@ -247,11 +248,15 @@ class MavenEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('Maven') - self.assertEqual('1.36.0', - ecosystem.next_version('io.grpc:grpc-core', '1.35.1')) - self.assertEqual('0.7.0', ecosystem.next_version('io.grpc:grpc-core', '0')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('blah:doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.36.0', + ecosystem.next_version('io.grpc:grpc-core', '1.35.1')) + self.assertEqual('0.7.0', ecosystem.next_version('io.grpc:grpc-core', + '0')) + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('blah:doesnotexist123456', '1') def test_enumerate(self): """Test enumerate.""" diff --git a/osv/ecosystems/minimos.py b/osv/ecosystems/minimos.py deleted file mode 100644 index 242c0726c7a..00000000000 --- a/osv/ecosystems/minimos.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""MinimOS ecosystem helper.""" - -from ..third_party.univers.alpine import AlpineLinuxVersion - -from .helper_base import Ecosystem - - -class MinimOS(Ecosystem): - """MinimOS packages ecosystem""" - - def sort_key(self, version): - # MinimOS uses `apk` package format - if not AlpineLinuxVersion.is_valid(version): - # If version is not valid, it is most likely an invalid input - # version then sort it to the last/largest element - return AlpineLinuxVersion('999999') - return AlpineLinuxVersion(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/minimos_test.py b/osv/ecosystems/minimos_test.py deleted file mode 100644 index a2734a5579b..00000000000 --- a/osv/ecosystems/minimos_test.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2025 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""MinimOS ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class MinimOSEcosystemTest(unittest.TestCase): - """MinimOS ecosystem helper tests.""" - - def test_minimos(self): - """Test sort_key""" - ecosystem = ecosystems.get('MinimOS') - self.assertGreater( - ecosystem.sort_key('38.52.0-r0'), ecosystem.sort_key('37.52.0-r0')) - self.assertLess(ecosystem.sort_key('453'), ecosystem.sort_key('453-r1')) - self.assertGreater(ecosystem.sort_key('5.4.13-r1'), ecosystem.sort_key('0')) - self.assertGreater( - ecosystem.sort_key('1.4.0-r1'), ecosystem.sort_key('1.4.0-r0')) - self.assertGreater( - ecosystem.sort_key('invalid'), ecosystem.sort_key('1.4.0-r0')) - self.assertGreater( - ecosystem.sort_key('13.0.14.5-r1'), ecosystem.sort_key('7.64.3-r2')) - self.assertLess( - ecosystem.sort_key('13.0.14.5-r1'), ecosystem.sort_key('16.6-r0')) diff --git a/osv/ecosystems/nuget.py b/osv/ecosystems/nuget.py index 90628e8175d..42fd15dcaab 100644 --- a/osv/ecosystems/nuget.py +++ b/osv/ecosystems/nuget.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import requests from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError from .. import semver_index # This relies on a strict SemVer implementation. @@ -80,7 +80,7 @@ def from_string(cls, str_version): return Version(semver_index.parse('999999'), 999999) -class NuGet(Ecosystem): +class NuGet(EnumerableEcosystem): """NuGet ecosystem.""" _API_PACKAGE_URL = ('https://api.nuget.org/v3/registration5-semver1/' @@ -126,7 +126,3 @@ def enumerate_versions(self, self.sort_versions(versions) return self._get_affected_versions(versions, introduced, fixed, last_affected, limits) - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/nuget_test.py b/osv/ecosystems/nuget_test.py index b1c633542c6..0ee29954b39 100644 --- a/osv/ecosystems/nuget_test.py +++ b/osv/ecosystems/nuget_test.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ import unittest import vcr.unittest +import warnings from . import nuget from .. import ecosystems @@ -90,16 +91,19 @@ class NuGetEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('NuGet') - self.assertEqual('3.0.1', - ecosystem.next_version('NuGet.Server.Core', '3.0.0')) - self.assertEqual('3.0.0.4001', - ecosystem.next_version('Castle.Core', '3.0.0.3001')) - self.assertEqual('3.1.0-RC', - ecosystem.next_version('Castle.Core', '3.0.0.4001')) - self.assertEqual('2.1.0-dev-00668', - ecosystem.next_version('Serilog', '2.1.0-dev-00666')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('3.0.1', + ecosystem.next_version('NuGet.Server.Core', '3.0.0')) + self.assertEqual('3.0.0.4001', + ecosystem.next_version('Castle.Core', '3.0.0.3001')) + self.assertEqual('3.1.0-RC', + ecosystem.next_version('Castle.Core', '3.0.0.4001')) + self.assertEqual('2.1.0-dev-00668', + ecosystem.next_version('Serilog', '2.1.0-dev-00666')) + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('doesnotexist123456', '1') def test_sort_key(self): ecosystem = ecosystems.get('NuGet') diff --git a/osv/ecosystems/openeuler.py b/osv/ecosystems/openeuler.py deleted file mode 100644 index c4c70093ea7..00000000000 --- a/osv/ecosystems/openeuler.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""openEuler ecosystem helper.""" - -from ..third_party.univers.rpm import RpmVersion - -from .helper_base import Ecosystem - - -class OpenEuler(Ecosystem): - """openEuler ecosystem""" - - @property - def name(self): - return "openEuler" - - def sort_key(self, version): - return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/openeuler_test.py b/osv/ecosystems/openeuler_test.py deleted file mode 100644 index f585679fd96..00000000000 --- a/osv/ecosystems/openeuler_test.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""openEuler ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class OpenEulerEcosystemTest(unittest.TestCase): - """openEuler ecosystem helper tests.""" - - def test_openeuler(self): - """Test sort key""" - ecosystem = ecosystems.get('openEuler') - self.assertEqual('openEuler', ecosystem.name) - self.assertGreater( - ecosystem.sort_key("1.2.3-1.oe2203"), - ecosystem.sort_key("1.2.2-1.oe2203")) - self.assertGreater( - ecosystem.sort_key("2.0.0-1.oe2203"), ecosystem.sort_key("0")) - self.assertGreater( - ecosystem.sort_key("1.2.3-2.oe2203"), - ecosystem.sort_key("1.2.3-1.oe2203")) - self.assertLess( - ecosystem.sort_key("1.2.2-1.oe2203"), - ecosystem.sort_key("1.2.3-1.oe2203")) - self.assertEqual( - ecosystem.sort_key("1.2.3-1.oe2203"), - ecosystem.sort_key("1.2.3-1.oe2203")) - self.assertLess(ecosystem.sort_key('invalid'), ecosystem.sort_key('0')) diff --git a/osv/ecosystems/opensuse.py b/osv/ecosystems/opensuse.py deleted file mode 100644 index 9754e634dac..00000000000 --- a/osv/ecosystems/opensuse.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""openSUSE ecosystem helper.""" - -from ..third_party.univers.rpm import RpmVersion - -from .helper_base import Ecosystem - - -class OpenSUSE(Ecosystem): - """"openSUSE ecosystem""" - - @property - def name(self): - return "openSUSE" - - def sort_key(self, version): - return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/opensuse_test.py b/osv/ecosystems/opensuse_test.py deleted file mode 100644 index 5e1b6cef9ee..00000000000 --- a/osv/ecosystems/opensuse_test.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""openSUSE ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class OpenSUSEEcosystemTest(unittest.TestCase): - """openSUSE ecosystem helper tests.""" - - def test_suse(self): - """Test sort key""" - ecosystem = ecosystems.get('openSUSE') - self.assertGreater( - ecosystem.sort_key("4.2-lp151.4.3.1"), - ecosystem.sort_key("1.5.1-lp151.4.3.1")) - self.assertGreater( - ecosystem.sort_key("4.9.6-bp152.2.3.1"), ecosystem.sort_key("0")) - self.assertGreater( - ecosystem.sort_key("6.2.8-bp156.2.3.1"), - ecosystem.sort_key("6.2.8-bp156")) - self.assertLess( - ecosystem.sort_key("0.4.6-15.8"), ecosystem.sort_key("1.4.6-15.8")) - self.assertEqual( - ecosystem.sort_key("6.2.8-bp156.2.3.1"), - ecosystem.sort_key("6.2.8-bp156.2.3.1")) diff --git a/osv/ecosystems/packagist.py b/osv/ecosystems/packagist.py index 60a9ee14e3d..5f79b19571b 100644 --- a/osv/ecosystems/packagist.py +++ b/osv/ecosystems/packagist.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ from typing import List from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError from ..request_helper import RequestError, RequestHelper @@ -194,7 +194,7 @@ def compare_special_versions(version_part_a: str, version_part_b: str) -> int: return 0 -class Packagist(Ecosystem): +class Packagist(EnumerableEcosystem): """Packagist ecosystem""" _API_PACKAGE_URL = 'https://repo.packagist.org/p2/{package}.json' diff --git a/osv/ecosystems/packagist_test.py b/osv/ecosystems/packagist_test.py index 3afca157ae0..dc87f0fe96b 100644 --- a/osv/ecosystems/packagist_test.py +++ b/osv/ecosystems/packagist_test.py @@ -1,4 +1,4 @@ -# Copyright 2022 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/osv/ecosystems/pub.py b/osv/ecosystems/pub.py index 01f1fdbebe6..0e000a7eec1 100644 --- a/osv/ecosystems/pub.py +++ b/osv/ecosystems/pub.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,7 +17,7 @@ import json from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError from .. import semver_index from ..request_helper import RequestError, RequestHelper @@ -31,7 +31,7 @@ # - Pre-release suffixes are evaluated before build suffixes. # - Versions with build suffixes come after versions without. # e.g. 1.0.0-pre < 1.0.0-pre+build < 1.0.0 < 1.0.0+build -# SemVer 2.0.0-rc.1 also does not explcitly disallow empty identifiers or +# SemVer 2.0.0-rc.1 also does not explicitly disallow empty identifiers or # leading 0s on numeric identifiers, but our SemVer implementation also will # parse these cases. @@ -66,7 +66,7 @@ def from_string(cls, str_version): return Version(semver_index.parse('999999')) -class Pub(Ecosystem): +class Pub(EnumerableEcosystem): """Pub ecosystem""" _API_PACKAGE_URL = 'https://pub.dev/api/packages/{package}' diff --git a/osv/ecosystems/pub_test.py b/osv/ecosystems/pub_test.py index 33493d1e3d8..19238d46308 100644 --- a/osv/ecosystems/pub_test.py +++ b/osv/ecosystems/pub_test.py @@ -1,4 +1,4 @@ -# Copyright 2023 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import unittest import vcr.unittest +import warnings from . import pub from .. import ecosystems @@ -106,28 +107,32 @@ class PubEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('Pub') - - self.assertEqual('2.0.0-nullsafety.0', - ecosystem.next_version('pub_semver', '1.4.4')) - self.assertEqual('2.0.0', - ecosystem.next_version('pub_semver', '2.0.0-nullsafety.0')) - self.assertEqual('2.1.0', ecosystem.next_version('pub_semver', '2.0.0')) - self.assertEqual('2.1.1', ecosystem.next_version('pub_semver', '2.1.0')) - - # Versions with pre-release and build suffixes. - self.assertEqual('3.0.0-alpha+2', - ecosystem.next_version('mockito', '3.0.0-alpha')) - self.assertEqual('3.0.0-alpha+3', - ecosystem.next_version('mockito', '3.0.0-alpha+2')) - self.assertEqual('3.0.0-beta', - ecosystem.next_version('mockito', '3.0.0-alpha+5')) - self.assertEqual('3.0.0', ecosystem.next_version('mockito', '3.0.0-beta+3')) - self.assertEqual('4.1.1+1', ecosystem.next_version('mockito', '4.1.1')) - self.assertEqual('4.1.2', ecosystem.next_version('mockito', '4.1.1+1')) - - # Version marked as retracted (go_router 4.2.1) - self.assertEqual('4.2.1', ecosystem.next_version('go_router', '4.2.0')) - self.assertEqual('4.2.2', ecosystem.next_version('go_router', '4.2.1')) + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + + self.assertEqual('2.0.0-nullsafety.0', + ecosystem.next_version('pub_semver', '1.4.4')) + self.assertEqual( + '2.0.0', ecosystem.next_version('pub_semver', '2.0.0-nullsafety.0')) + self.assertEqual('2.1.0', ecosystem.next_version('pub_semver', '2.0.0')) + self.assertEqual('2.1.1', ecosystem.next_version('pub_semver', '2.1.0')) + + # Versions with pre-release and build suffixes. + self.assertEqual('3.0.0-alpha+2', + ecosystem.next_version('mockito', '3.0.0-alpha')) + self.assertEqual('3.0.0-alpha+3', + ecosystem.next_version('mockito', '3.0.0-alpha+2')) + self.assertEqual('3.0.0-beta', + ecosystem.next_version('mockito', '3.0.0-alpha+5')) + self.assertEqual('3.0.0', ecosystem.next_version('mockito', + '3.0.0-beta+3')) + self.assertEqual('4.1.1+1', ecosystem.next_version('mockito', '4.1.1')) + self.assertEqual('4.1.2', ecosystem.next_version('mockito', '4.1.1+1')) + + # Version marked as retracted (go_router 4.2.1) + self.assertEqual('4.2.1', ecosystem.next_version('go_router', '4.2.0')) + self.assertEqual('4.2.2', ecosystem.next_version('go_router', '4.2.1')) if __name__ == '__main__': diff --git a/osv/ecosystems/pypi.py b/osv/ecosystems/pypi.py index 5f9d82451f1..7d82624829b 100644 --- a/osv/ecosystems/pypi.py +++ b/osv/ecosystems/pypi.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,10 +17,10 @@ import requests from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError -class PyPI(Ecosystem): +class PyPI(EnumerableEcosystem): """PyPI ecosystem helpers.""" _API_PACKAGE_URL = 'https://pypi.org/pypi/{package}/json' @@ -52,7 +52,3 @@ def enumerate_versions(self, return self._get_affected_versions(versions, introduced, fixed, last_affected, limits) - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/pypi_test.py b/osv/ecosystems/pypi_test.py index e20c250cb90..935f5aade90 100644 --- a/osv/ecosystems/pypi_test.py +++ b/osv/ecosystems/pypi_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ """PyPI ecosystem helper tests.""" import vcr.unittest +import warnings from .. import ecosystems @@ -24,11 +25,14 @@ class PyPIEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('PyPI') - self.assertEqual('1.36.0rc1', ecosystem.next_version('grpcio', '1.35.0')) - self.assertEqual('1.36.1', ecosystem.next_version('grpcio', '1.36.0')) - self.assertEqual('0.3.0', ecosystem.next_version('grpcio', '0')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.36.0rc1', ecosystem.next_version('grpcio', '1.35.0')) + self.assertEqual('1.36.1', ecosystem.next_version('grpcio', '1.36.0')) + self.assertEqual('0.3.0', ecosystem.next_version('grpcio', '0')) + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('doesnotexist123456', '1') def test_sort_key(self): """Test sort_key""" diff --git a/osv/ecosystems/redhat.py b/osv/ecosystems/redhat.py index 3d3cf0a7aa7..c83a1758782 100644 --- a/osv/ecosystems/redhat.py +++ b/osv/ecosystems/redhat.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,27 +14,11 @@ """Red Hat Linux ecosystem helper.""" from ..third_party.univers.rpm import RpmVersion -from .helper_base import Ecosystem +from .ecosystems_base import OrderedEcosystem -class RedHat(Ecosystem): - """Red Hat Linux ecosystem""" - - @property - def name(self): - return 'Red Hat' +class RPM(OrderedEcosystem): + """Red Hat Package Manager ecosystem helper.""" def sort_key(self, version): return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/redhat_test.py b/osv/ecosystems/redhat_test.py index 2972035787d..b99eccbfbfd 100644 --- a/osv/ecosystems/redhat_test.py +++ b/osv/ecosystems/redhat_test.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -11,19 +11,22 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. -"""Red Hat Linux ecosystem helper tests.""" +"""RPM / Red Hat Linux ecosystem helper tests.""" import unittest + +from . import redhat + from .. import ecosystems -class RedHatEcosystemTest(unittest.TestCase): - """Red Hat Linux ecosystem helper tests.""" +class RPMEcosystemTest(unittest.TestCase): + """RPM ecosystem helper tests.""" - def test_redhat(self): + def test_rpm(self): """Test sort_key""" - ecosystem = ecosystems.get('Red Hat') - self.assertEqual('Red Hat', ecosystem.name) + ecosystem = redhat.RPM() + # Red Hat self.assertGreater( ecosystem.sort_key('0:0.2.6-20.module+el8.9.0+1420+91577025'), ecosystem.sort_key('0:0.0.99.4-5.module+el8.9.0+1445+07728297')) @@ -34,3 +37,91 @@ def test_redhat(self): ecosystem.sort_key('2:1.14.3-2.module+el8.10.0+1815+5fe7415e'), ecosystem.sort_key('2:1.10.3-1.module+el8.10.0+1815+5fe7415e')) self.assertLess(ecosystem.sort_key('invalid'), ecosystem.sort_key('0')) + + # AlmaLinux + self.assertGreater( + ecosystem.sort_key("9.27-15.el8_10"), + ecosystem.sort_key("9.27-13.el8_10")) + self.assertGreater( + ecosystem.sort_key("9.27-15.el8_10"), ecosystem.sort_key("0")) + self.assertGreater( + ecosystem.sort_key("3:2.1.10-1.module_el8.10.0+3858+6ad51f9f"), + ecosystem.sort_key("3:2.1.10-1.module_el8.10.0+3845+87b84552")) + self.assertLess( + ecosystem.sort_key("20230404-117.git2e92a49f.el8_8.alma.1"), + ecosystem.sort_key("20240111-121.gitb3132c18.el8")) + self.assertEqual( + ecosystem.sort_key("20240111-121.gitb3132c18.el8"), + ecosystem.sort_key("20240111-121.gitb3132c18.el8")) + + # Mageia + self.assertGreater( + ecosystem.sort_key('3.2.7-1.2.mga9'), + ecosystem.sort_key('3.2.7-1.mga9')) + self.assertGreater( + ecosystem.sort_key('3.2.7-1.2.mga9'), ecosystem.sort_key('0')) + self.assertLess(ecosystem.sort_key('invalid'), ecosystem.sort_key('0')) + self.assertGreater( + ecosystem.sort_key('1:1.8.11-1.mga9'), + ecosystem.sort_key('0:1.9.1-2.mga9')) + + # openEuler + self.assertGreater( + ecosystem.sort_key("1.2.3-1.oe2203"), + ecosystem.sort_key("1.2.2-1.oe2203")) + self.assertGreater( + ecosystem.sort_key("2.0.0-1.oe2203"), ecosystem.sort_key("0")) + self.assertGreater( + ecosystem.sort_key("1.2.3-2.oe2203"), + ecosystem.sort_key("1.2.3-1.oe2203")) + self.assertLess( + ecosystem.sort_key("1.2.2-1.oe2203"), + ecosystem.sort_key("1.2.3-1.oe2203")) + self.assertEqual( + ecosystem.sort_key("1.2.3-1.oe2203"), + ecosystem.sort_key("1.2.3-1.oe2203")) + + # openSUSE + self.assertGreater( + ecosystem.sort_key("4.2-lp151.4.3.1"), + ecosystem.sort_key("1.5.1-lp151.4.3.1")) + self.assertGreater( + ecosystem.sort_key("4.9.6-bp152.2.3.1"), ecosystem.sort_key("0")) + self.assertGreater( + ecosystem.sort_key("6.2.8-bp156.2.3.1"), + ecosystem.sort_key("6.2.8-bp156")) + self.assertLess( + ecosystem.sort_key("0.4.6-15.8"), ecosystem.sort_key("1.4.6-15.8")) + self.assertEqual( + ecosystem.sort_key("6.2.8-bp156.2.3.1"), + ecosystem.sort_key("6.2.8-bp156.2.3.1")) + + # SUSE + self.assertGreater( + ecosystem.sort_key("2.38.5-150400.4.34.2"), + ecosystem.sort_key("2.37.5-150400.4.34.2")) + self.assertGreater( + ecosystem.sort_key("2.0.8-4.8.2"), ecosystem.sort_key("0")) + self.assertGreater( + ecosystem.sort_key("2.0.8_k4.12.14_10.118-4.8.2"), + ecosystem.sort_key("2.0.8-4.8.2")) + self.assertLess( + ecosystem.sort_key("1.86-150100.7.23.11"), + ecosystem.sort_key("2.86-150100.7.23.1")) + self.assertEqual( + ecosystem.sort_key("2.0.8-4.8.2"), ecosystem.sort_key("2.0.8-4.8.2")) + + def test_rpm_ecosystems(self): + """Test RPM-based ecosystems return an RPM ecosystem.""" + ecos = [ + 'Red Hat', + 'AlmaLinux', + 'Mageia', + 'openEuler', + 'openSUSE', + 'Rocky Linux', + 'SUSE', + ] + for ecosystem_name in ecos: + ecosystem = ecosystems.get(ecosystem_name) + self.assertIsInstance(ecosystem, redhat.RPM) diff --git a/osv/ecosystems/rocky_linux.py b/osv/ecosystems/rocky_linux.py deleted file mode 100644 index 8f59072ae59..00000000000 --- a/osv/ecosystems/rocky_linux.py +++ /dev/null @@ -1,40 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Rocky linux ecosystem helper.""" - -from ..third_party.univers.rpm import RpmVersion -from .helper_base import Ecosystem - - -class RockyLinux(Ecosystem): - """Rocky Linux ecosystem""" - - @property - def name(self): - return 'Rocky Linux' - - def sort_key(self, version): - return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/rocky_linux_test.py b/osv/ecosystems/rocky_linux_test.py deleted file mode 100644 index b54c9f95c36..00000000000 --- a/osv/ecosystems/rocky_linux_test.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Rocky Linux ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class RockyLinuxEcosystemTest(unittest.TestCase): - """Rocky Linux ecosystem helper tests.""" - - def test_rocky_linux(self): - """Test sort_key""" - ecosystem = ecosystems.get('Rocky Linux') - self.assertEqual('Rocky Linux', ecosystem.name) - self.assertGreater( - ecosystem.sort_key('0:0.2.6-20.module+el8.9.0+1420+91577025'), - ecosystem.sort_key('0:0.0.99.4-5.module+el8.9.0+1445+07728297')) - self.assertGreater( - ecosystem.sort_key('0:0.2.6-20.module+el8.9.0+1420+91577025'), - ecosystem.sort_key('0')) - self.assertGreater( - ecosystem.sort_key('2:1.14.3-2.module+el8.10.0+1815+5fe7415e'), - ecosystem.sort_key('2:1.10.3-1.module+el8.10.0+1815+5fe7415e')) - self.assertLess(ecosystem.sort_key('invalid'), ecosystem.sort_key('0')) diff --git a/osv/ecosystems/rubygems.py b/osv/ecosystems/rubygems.py index 23ef1fc4cb7..7a65771431e 100644 --- a/osv/ecosystems/rubygems.py +++ b/osv/ecosystems/rubygems.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -18,10 +18,10 @@ from ..third_party.univers.gem import GemVersion, InvalidVersionError from . import config -from .helper_base import Ecosystem, EnumerateError +from .ecosystems_base import EnumerableEcosystem, EnumerateError -class RubyGems(Ecosystem): +class RubyGems(EnumerableEcosystem): """RubyGems ecosystem.""" _API_PACKAGE_URL = 'https://rubygems.org/api/v1/versions/{package}.json' @@ -57,7 +57,3 @@ def enumerate_versions(self, self.sort_versions(versions) return self._get_affected_versions(versions, introduced, fixed, last_affected, limits) - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/rubygems_test.py b/osv/ecosystems/rubygems_test.py index 620b9a88b70..755c904f3a2 100644 --- a/osv/ecosystems/rubygems_test.py +++ b/osv/ecosystems/rubygems_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ """RubyGems ecosystem helper tests.""" import vcr.unittest +import warnings from .. import ecosystems @@ -24,15 +25,18 @@ class RubyGemsEcosystemTest(vcr.unittest.VCRTestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('RubyGems') - self.assertEqual('0.8.0', ecosystem.next_version('rails', '0')) - self.assertEqual('0.9.5', ecosystem.next_version('rails', '0.9.4.1')) - self.assertEqual('2.3.8.pre1', ecosystem.next_version('rails', '2.3.7')) - self.assertEqual('4.0.0.rc1', - ecosystem.next_version('rails', '4.0.0.beta1')) - self.assertEqual('5.0.0.racecar1', - ecosystem.next_version('rails', '5.0.0.beta4')) - with self.assertRaises(ecosystems.EnumerateError): - ecosystem.next_version('doesnotexist123456', '1') + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('0.8.0', ecosystem.next_version('rails', '0')) + self.assertEqual('0.9.5', ecosystem.next_version('rails', '0.9.4.1')) + self.assertEqual('2.3.8.pre1', ecosystem.next_version('rails', '2.3.7')) + self.assertEqual('4.0.0.rc1', + ecosystem.next_version('rails', '4.0.0.beta1')) + self.assertEqual('5.0.0.racecar1', + ecosystem.next_version('rails', '5.0.0.beta4')) + with self.assertRaises(ecosystems.EnumerateError): + ecosystem.next_version('doesnotexist123456', '1') def test_sort_key(self): """Test sort_key with invalid versions""" diff --git a/osv/ecosystems/semver_ecosystem_helper.py b/osv/ecosystems/semver_ecosystem_helper.py index 12f03068d09..4a787b17be3 100644 --- a/osv/ecosystems/semver_ecosystem_helper.py +++ b/osv/ecosystems/semver_ecosystem_helper.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -12,13 +12,15 @@ # See the License for the specific language governing permissions and # limitations under the License. """Ecosystem helper for ecosystems using SemVer.""" +from warnings import deprecated -from .helper_base import Ecosystem +from .ecosystems_base import OrderedEcosystem from .. import semver_index -class SemverEcosystem(Ecosystem): - """Generic semver ecosystem helpers.""" +class SemverLike(OrderedEcosystem): + """Ecosystem helper for ecosystems that use SEMVER-compatible versioning, + but use the ECOSYSTEM version type.""" def sort_key(self, version): """Sort key.""" @@ -29,18 +31,12 @@ def sort_key(self, version): # treat it as a very large version so as to not match anything. return semver_index.parse('999999') - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - """Enumerate versions (no-op).""" - del package - del introduced - del fixed - del limits +class SemverEcosystem(SemverLike): + """Ecosystems which use the 'SEMVER' OSV version type""" + + @deprecated('Avoid using this method. ' + 'It is provided only to maintain existing tooling.') def next_version(self, package, version): """Get the next version after the given version.""" del package # Unused. @@ -49,7 +45,3 @@ def next_version(self, package, version): return version + '.0' return str(parsed_version.bump_patch()) + '-0' - - @property - def is_semver(self): - return True diff --git a/osv/ecosystems/semver_ecosystem_helper_test.py b/osv/ecosystems/semver_ecosystem_helper_test.py index 27352f56f12..38f2015c2dd 100644 --- a/osv/ecosystems/semver_ecosystem_helper_test.py +++ b/osv/ecosystems/semver_ecosystem_helper_test.py @@ -1,4 +1,4 @@ -# Copyright 2021 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -14,6 +14,7 @@ """SemVer-based ecosystem helper tests.""" import unittest +import warnings from .. import ecosystems @@ -24,5 +25,9 @@ class SemVerEcosystemTest(unittest.TestCase): def test_next_version(self): """Test next_version.""" ecosystem = ecosystems.get('Go') - self.assertEqual('1.0.1-0', ecosystem.next_version('blah', '1.0.0')) - self.assertEqual('1.0.0-pre.0', ecosystem.next_version('blah', '1.0.0-pre')) + with warnings.catch_warnings(): + # Filter the DeprecationWarning from next_version + warnings.filterwarnings('ignore', 'Avoid using this method') + self.assertEqual('1.0.1-0', ecosystem.next_version('blah', '1.0.0')) + self.assertEqual('1.0.0-pre.0', + ecosystem.next_version('blah', '1.0.0-pre')) diff --git a/osv/ecosystems/suse.py b/osv/ecosystems/suse.py deleted file mode 100644 index e55b942bc4f..00000000000 --- a/osv/ecosystems/suse.py +++ /dev/null @@ -1,37 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""SUSE ecosystem helper.""" - -from ..third_party.univers.rpm import RpmVersion - -from .helper_base import Ecosystem - - -class SUSE(Ecosystem): - """"SUSE ecosystem""" - - def sort_key(self, version): - return RpmVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/suse_test.py b/osv/ecosystems/suse_test.py deleted file mode 100644 index 2dfc787b4d6..00000000000 --- a/osv/ecosystems/suse_test.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""SUSE ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class SUSEEcosystemTest(unittest.TestCase): - """SUSE ecosystem helper tests.""" - - def test_suse(self): - """Test sort key""" - ecosystem = ecosystems.get('SUSE') - self.assertGreater( - ecosystem.sort_key("2.38.5-150400.4.34.2"), - ecosystem.sort_key("2.37.5-150400.4.34.2")) - self.assertGreater( - ecosystem.sort_key("2.0.8-4.8.2"), ecosystem.sort_key("0")) - self.assertGreater( - ecosystem.sort_key("2.0.8_k4.12.14_10.118-4.8.2"), - ecosystem.sort_key("2.0.8-4.8.2")) - self.assertLess( - ecosystem.sort_key("1.86-150100.7.23.11"), - ecosystem.sort_key("2.86-150100.7.23.1")) - self.assertEqual( - ecosystem.sort_key("2.0.8-4.8.2"), ecosystem.sort_key("2.0.8-4.8.2")) diff --git a/osv/ecosystems/ubuntu.py b/osv/ecosystems/ubuntu.py index cf7cf5db07b..ac9959b2cb7 100644 --- a/osv/ecosystems/ubuntu.py +++ b/osv/ecosystems/ubuntu.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -15,21 +15,13 @@ from ..third_party.univers.debian import Version as UbuntuVersion -from .helper_base import Ecosystem +from .ecosystems_base import OrderedEcosystem -class Ubuntu(Ecosystem): +class Ubuntu(OrderedEcosystem): """Ubuntu ecosystem""" def sort_key(self, version): if not UbuntuVersion.is_valid(version): return UbuntuVersion(999999, '999999') return UbuntuVersion.from_string(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') diff --git a/osv/ecosystems/ubuntu_test.py b/osv/ecosystems/ubuntu_test.py index 8224319e1eb..8343589ae20 100644 --- a/osv/ecosystems/ubuntu_test.py +++ b/osv/ecosystems/ubuntu_test.py @@ -1,4 +1,4 @@ -# Copyright 2024 Google LLC +# Copyright 2025 Google LLC # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/osv/ecosystems/wolfi.py b/osv/ecosystems/wolfi.py deleted file mode 100644 index 6d64a9629b0..00000000000 --- a/osv/ecosystems/wolfi.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Wolfi ecosystem helper.""" - -from ..third_party.univers.alpine import AlpineLinuxVersion - -from .helper_base import Ecosystem - - -class Wolfi(Ecosystem): - """Wolfi packages ecosystem""" - - def sort_key(self, version): - # Wolfi uses `apk` package format - if not AlpineLinuxVersion.is_valid(version): - # If version is not valid, it is most likely an invalid input - # version then sort it to the last/largest element - return AlpineLinuxVersion('999999') - return AlpineLinuxVersion(version) - - def enumerate_versions(self, - package, - introduced, - fixed=None, - last_affected=None, - limits=None): - raise NotImplementedError('Ecosystem helper does not support enumeration') - - @property - def supports_comparing(self): - return True diff --git a/osv/ecosystems/wolfi_test.py b/osv/ecosystems/wolfi_test.py deleted file mode 100644 index b6e4e11cae3..00000000000 --- a/osv/ecosystems/wolfi_test.py +++ /dev/null @@ -1,33 +0,0 @@ -# Copyright 2024 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Wolfi ecosystem helper tests.""" - -import unittest -from .. import ecosystems - - -class WolfiEcosystemTest(unittest.TestCase): - """Wolfi ecosystem helper tests.""" - - def test_wolfi(self): - """Test sort_key""" - ecosystem = ecosystems.get('Wolfi') - self.assertGreater( - ecosystem.sort_key('38.52.0-r0'), ecosystem.sort_key('37.52.0-r0')) - self.assertLess(ecosystem.sort_key('453'), ecosystem.sort_key('453-r1')) - self.assertGreater(ecosystem.sort_key('5.4.13-r1'), ecosystem.sort_key('0')) - self.assertGreater( - ecosystem.sort_key('1.4.0-r1'), ecosystem.sort_key('1.4.0-r0')) - self.assertGreater( - ecosystem.sort_key('invalid'), ecosystem.sort_key('1.4.0-r0')) diff --git a/osv/impact.py b/osv/impact.py index 788c9e0e15a..933bc3ebbd1 100644 --- a/osv/impact.py +++ b/osv/impact.py @@ -728,7 +728,7 @@ def analyze(vulnerability: vulnerability_pb2.Vulnerability, versions = [] for affected_range in affected.ranges: if (affected_range.type == vulnerability_pb2.Range.ECOSYSTEM and - affected.package.ecosystem in ecosystems.SEMVER_ECOSYSTEMS): + ecosystems.is_semver(affected.package.ecosystem)): # Replace erroneous range type. affected_range.type = vulnerability_pb2.Range.SEMVER @@ -736,7 +736,10 @@ def analyze(vulnerability: vulnerability_pb2.Vulnerability, vulnerability_pb2.Range.SEMVER): # Enumerate ECOSYSTEM and SEMVER ranges. ecosystem_helpers = ecosystems.get(affected.package.ecosystem) - if ecosystem_helpers and ecosystem_helpers.supports_ordering: + if ecosystem_helpers is None: + logging.warning('No ecosystem helpers implemented for %s: %s', + affected.package.ecosystem, vulnerability.id) + elif isinstance(ecosystem_helpers, ecosystems.EnumerableEcosystem): try: versions.extend( enumerate_versions(affected.package.name, ecosystem_helpers, @@ -745,12 +748,6 @@ def analyze(vulnerability: vulnerability_pb2.Vulnerability, # Allow non-retryable enumeration errors to occur (e.g. if the # package no longer exists). pass - except NotImplementedError: - # Some ecosystems support ordering but don't support enumeration. - pass - else: - logging.warning('No ecosystem helpers implemented for %s: %s', - affected.package.ecosystem, vulnerability.id) new_git_versions = set() new_introduced = set() diff --git a/osv/models.py b/osv/models.py index 4ad81253201..fe9054d2a7a 100644 --- a/osv/models.py +++ b/osv/models.py @@ -453,7 +453,7 @@ def _pre_put_hook(self): # pylint: disable=arguments-differ for affected_package in self.affected_packages: # Indexes used for querying by exact version. ecosystem_helper = ecosystems.get(affected_package.package.ecosystem) - if ecosystem_helper and ecosystem_helper.supports_ordering: + if ecosystem_helper is not None: # No need to normalize if the ecosystem is supported. self.affected_fuzzy.extend(affected_package.versions) else: @@ -1168,10 +1168,6 @@ def affected_from_bug(entity: Bug) -> list[AffectedVersions]: # Ecosystem helper for sorting the events. e_helper = ecosystems.get(pkg_ecosystem) - if e_helper is not None and not (e_helper.supports_comparing or - e_helper.is_semver): - e_helper = None - # TODO(michaelkedar): I am matching the current behaviour of the API, # where GIT tags match to the first git repo in the ranges list, even if # there are non-git ranges or multiple git repos in a range. @@ -1465,7 +1461,7 @@ def sorted_events(ecosystem, range_type, events) -> list[AffectedEvent]: else: ecosystem_helper = ecosystems.get(ecosystem) - if ecosystem_helper is None or not ecosystem_helper.supports_ordering: + if ecosystem_helper is None: raise ValueError('Unsupported ecosystem ' + ecosystem) # Remove any magic '0' values.