From 3491a6532c217b91c6729ebf2dc2fed4a41e15e9 Mon Sep 17 00:00:00 2001 From: Keshav Priyadarshi Date: Thu, 24 Feb 2022 01:23:13 +0530 Subject: [PATCH] Add openssl support in univers - closes https://github.com/nexB/univers/issues/36 - For `OpenSSL-FIPS Module` see https://github.com/nexB/univers/issues/41 Signed-off-by: Keshav Priyadarshi --- src/univers/version_constraint.py | 16 ++++++ src/univers/version_range.py | 23 +++++++++ src/univers/versions.py | 84 +++++++++++++++++++++++++++++++ tests/test_version_range.py | 32 ++++++++++++ 4 files changed, 155 insertions(+) diff --git a/src/univers/version_constraint.py b/src/univers/version_constraint.py index 1b239b3a..5e46489d 100644 --- a/src/univers/version_constraint.py +++ b/src/univers/version_constraint.py @@ -157,6 +157,22 @@ def from_string(cls, string, version_class): version = None else: version = version_class(version) + + """ + Can't always be certain that a particular class would create an object + of its own kind it may return an object of a different class. + + Example: + OpensslVersion when instantiated will return either an object of + LegacyOpensslVersion or SemverVersion depending on version. + OpensslVersion("0.9.8a") -> LegacyOpensslVersion(string="0.9.8a") + OpensslVersion("3.0.1") -> SemverVersion(string="3.0.1") + + Hence the need for updating `version_class` after creation of version object. + """ + if version != None: + version_class = version.__class__ + return cls(comparator=comparator, version=version, version_class=version_class) @staticmethod diff --git a/src/univers/version_range.py b/src/univers/version_range.py index 705b9d0b..daa1033c 100644 --- a/src/univers/version_range.py +++ b/src/univers/version_range.py @@ -868,6 +868,28 @@ def from_native(cls, string): return cls(constraints=constraints) +class OpensslVersionRange(VersionRange): + """ + openssl doesn't use <,>,<= or >=. + For more 'https://www.openssl.org/news/vulnerabilities.xml'. + """ + + scheme = "openssl" + version_class = versions.OpensslVersion + vers_by_native_comparators = {"=": "="} + + @classmethod + def from_native(cls, string): + cleaned = remove_spaces(string).lower() + constraints = set() + # plain single version + for clause in cleaned.split(","): + version = cls.version_class(clause) + constraint = VersionConstraint(comparator="=", version=version) + constraints.add(constraint) + return cls(constraints=list(constraints)) + + def is_even(s): """ Return True if the string "s" is an even number and False if this is an odd @@ -902,4 +924,5 @@ def is_even(s): "ebuild": EbuildVersionRange, "archlinux": ArchLinuxVersionRange, "nginx": NginxVersionRange, + "openssl": OpensslVersionRange, } diff --git a/src/univers/versions.py b/src/univers/versions.py index 6ce4262e..be25c334 100644 --- a/src/univers/versions.py +++ b/src/univers/versions.py @@ -5,6 +5,7 @@ # Visit https://aboutcode.org and https://github.com/nexB/univers for support and download. from functools import total_ordering +import re import attr import semantic_version @@ -343,3 +344,86 @@ def __gt__(self, other): if not isinstance(other, self.__class__): return NotImplemented return gentoo.vercmp(self.value, other.value) > 0 + + +@attr.s(frozen=True, order=False, eq=False, hash=True) +class LegacyOpensslVersion(Version): + @classmethod + def is_valid(cls, string): + return OpensslVersion.is_valid_legacy(string) + + def __eq__(self, other): + if isinstance(other, SemverVersion): + return False + if not isinstance(other, self.__class__): + return NotImplemented + return self.value == other.value + + def __lt__(self, other): + """ + All legacy scheme is behind those using semver. + """ + if isinstance(other, SemverVersion): + return True + if not isinstance(other, self.__class__): + return NotImplemented + re_vers1 = re.findall(r"^(\d+\.\d+\.\d+)(.*)$", self.value)[0] + re_vers2 = re.findall(r"^(\d+\.\d+\.\d+)(.*)$", other.value)[0] + if int(re_vers1[0].replace(".", "")) < int(re_vers2[0].replace(".", "")): + return True + return re_vers1[0] == re_vers2[0] and re_vers1[1] < re_vers2[1] + + def __gt__(self, other): + """ + All openssl version using semver is ahead of legacy scheme. + """ + if isinstance(other, SemverVersion): + return False + if not isinstance(other, self.__class__): + return NotImplemented + re_vers1 = re.findall(r"^(\d+\.\d+\.\d+)([a-z-]+)$", self.value)[0] + re_vers2 = re.findall(r"^(\d+\.\d+\.\d+)([a-z-]+)$", other.value)[0] + if int(re_vers1[0].replace(".", "")) > int(re_vers2[0].replace(".", "")): + return True + return re_vers1[0] == re_vers2[0] and re_vers1[1] > re_vers2[1] + + def __le__(self, other): + return self.__lt__(other) or self.__eq__(other) + + def __ge__(self, other): + return self.__gt__(other) or self.__eq__(other) + + +@attr.s(frozen=True, order=False, eq=False, hash=True) +class OpensslVersion: + """ + Handles the object creation for openssl depending on + whether version is legacy or semver. + """ + + def __new__(cls, string): + if OpensslVersion.is_valid_legacy(string): + return LegacyOpensslVersion(string) + elif OpensslVersion.is_valid_new_openssl(string): + return SemverVersion(string) + raise InvalidVersion(f"{string!r} is not a valid/supported OpensslVersion") + + @classmethod + def is_valid_legacy(cls, string): + """ + All legacy base version of openssl that ever exited/exists. + """ + all_legacy_base = ["0.9.6", "0.9.7", "0.9.8", "1.0.0", "1.0.1", "1.0.2", "1.1.0", "1.1.1"] + if string in all_legacy_base: + return True + regexed_string = re.findall(r"^(\d+\.\d+\.\d+)([a-z-]+)$", string) + if not regexed_string or regexed_string[0][0] not in all_legacy_base: + return False + return True + + @classmethod + def is_valid_new_openssl(cls, string): + regexed_string = re.findall(r"^(\d+)(\.)(.*)$", string) + if not regexed_string or int(regexed_string[0][0]) < 3: + return False + return SemverVersion.is_valid(string) diff --git a/tests/test_version_range.py b/tests/test_version_range.py index 4b2754c0..9d125fbe 100644 --- a/tests/test_version_range.py +++ b/tests/test_version_range.py @@ -14,9 +14,11 @@ from univers.version_range import VersionRange from univers.version_range import RANGE_CLASS_BY_SCHEMES from univers.version_range import NpmVersionRange +from univers.version_range import OpensslVersionRange from univers.versions import PypiVersion from univers.versions import RubygemsVersion from univers.versions import SemverVersion +from univers.versions import LegacyOpensslVersion class TestVersionRange(TestCase): @@ -233,10 +235,40 @@ def test_NpmVersionRange_from_native_with_approximately_equal_to_operator(self): version_range = NpmVersionRange.from_native(npm_range) assert version_range == expected + def test_OpensslVersionRange_from_native_single_legacy(self): + openssl_range = "0.9.8j" + expected = OpensslVersionRange( + constraints=( + VersionConstraint(comparator="=", version=LegacyOpensslVersion(string="0.9.8j")), + ) + ) + version_range = OpensslVersionRange.from_native(openssl_range) + assert version_range == expected + + def test_OpensslVersionRange_from_native_single_new_semver(self): + openssl_range = "3.0.1" + expected = OpensslVersionRange( + constraints=(VersionConstraint(comparator="=", version=SemverVersion(string="3.0.1")),) + ) + version_range = OpensslVersionRange.from_native(openssl_range) + assert version_range == expected + + def test_OpensslVersionRange_from_native_mixed(self): + openssl_range = "3.0.0, 1.0.1b" + expected = OpensslVersionRange( + constraints=( + VersionConstraint(comparator="=", version=LegacyOpensslVersion(string="1.0.1b")), + VersionConstraint(comparator="=", version=SemverVersion(string="3.0.0")), + ) + ) + version_range = OpensslVersionRange.from_native(openssl_range) + assert version_range == expected + VERSION_RANGE_TESTS_BY_SCHEME = { "nginx": ["0.8.40+", "0.7.52-0.8.39", "0.9.10", "1.5.0+, 1.4.1+"], "npm": ["^1.2.9", "~3.8.2", "5.0.0 - 7.2.3", "2.1 || 2.6", "1.1.2 1.2.2", "<=2.1 >=1.1"], + "openssl": ["1.1.1ak", "1.1.0", "3.0.2", "3.0.1, 0.9.7a", "1.0.2ck, 3.1.2"], }