cve-check: replace Looseversion with custom version class

The way distutils.version.LooseVersion compare version are tricky, it treat
all these ( "1.0-beta2", "1.0-rc1", "1.0A", "1.0p2" and "1.0pre1") as greater
version than "1.0". This might be right for "1.0A" and "1.0p1" but not for
the rest, also these version could be confusing, the "p" in "1.0p1" can be
"pre" or "patched" version or even other meaning.

Replace Looseversion with custom class, it uses regex to capture common
version format like "1.1.1" or tag format using date like "2020-12-12" as
release section, check for following known string/tags ( beta, rc, pre, dev,
alpha, preview) as pre-release section, any other trailing characters
are difficult to understand/define so ignore them. Compare release
section and pre-release section saperately.

included selftest for the version class.

[YOCTO#14127]

(From OE-Core rev: 294baea424472341d2ec880f13699076315d8274)

Signed-off-by: Lee Chee Yang <chee.yang.lee@intel.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
(cherry picked from commit 6ced85e9ddd3569240f1e8b82130d1ac0fffbc40)
Signed-off-by: Steve Sakoman <steve@sakoman.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
This commit is contained in:
Lee Chee Yang
2021-01-22 18:07:19 +08:00
committed by Richard Purdie
parent 4b4d1dac11
commit f829419105
3 changed files with 90 additions and 5 deletions

View File

@@ -203,7 +203,7 @@ def check_cves(d, patched_cves):
"""
Connect to the NVD database and find unpatched cves.
"""
from distutils.version import LooseVersion
from oe.cve_check import Version
pn = d.getVar("PN")
real_pv = d.getVar("PV")
@@ -260,8 +260,8 @@ def check_cves(d, patched_cves):
else:
if operator_start:
try:
vulnerable_start = (operator_start == '>=' and LooseVersion(pv) >= LooseVersion(version_start))
vulnerable_start |= (operator_start == '>' and LooseVersion(pv) > LooseVersion(version_start))
vulnerable_start = (operator_start == '>=' and Version(pv) >= Version(version_start))
vulnerable_start |= (operator_start == '>' and Version(pv) > Version(version_start))
except:
bb.warn("%s: Failed to compare %s %s %s for %s" %
(product, pv, operator_start, version_start, cve))
@@ -271,8 +271,8 @@ def check_cves(d, patched_cves):
if operator_end:
try:
vulnerable_end = (operator_end == '<=' and LooseVersion(pv) <= LooseVersion(version_end))
vulnerable_end |= (operator_end == '<' and LooseVersion(pv) < LooseVersion(version_end))
vulnerable_end = (operator_end == '<=' and Version(pv) <= Version(version_end) )
vulnerable_end |= (operator_end == '<' and Version(pv) < Version(version_end) )
except:
bb.warn("%s: Failed to compare %s %s %s for %s" %
(product, pv, operator_end, version_end, cve))

58
meta/lib/oe/cve_check.py Normal file
View File

@@ -0,0 +1,58 @@
import collections
import re
import itertools
_Version = collections.namedtuple(
"_Version", ["release", "pre_l", "pre_v"]
)
class Version():
_version_pattern = r"""v?(?:(?P<release>[0-9]+(?:[-\.][0-9]+)*)(?P<pre>[-_\.]?(?P<pre_l>(rc|alpha|beta|pre|preview|dev))[-_\.]?(?P<pre_v>[0-9]+)?)?)(.*)?"""
_regex = re.compile(r"^\s*" + _version_pattern + r"\s*$", re.VERBOSE | re.IGNORECASE)
def __init__(self, version):
match = self._regex.search(version)
if not match:
raise Exception("Invalid version: '{0}'".format(version))
self._version = _Version(
release=tuple(int(i) for i in match.group("release").replace("-",".").split(".")),
pre_l=match.group("pre_l"),
pre_v=match.group("pre_v")
)
self._key = _cmpkey(
self._version.release,
self._version.pre_l,
self._version.pre_v
)
def __le__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key <= other._key
def __lt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key < other._key
def __ge__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key >= other._key
def __gt__(self, other):
if not isinstance(other, Version):
return NotImplemented
return self._key > other._key
def _cmpkey(release, pre_l, pre_v):
# remove leading 0
_release = tuple(
reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release))))
)
if pre_l is None and pre_v is None:
_pre = float('inf')
else:
_pre = float(pre_v) if pre_v else float('-inf')
return _release, _pre

View File

@@ -0,0 +1,27 @@
from oe.cve_check import Version
from oeqa.selftest.case import OESelftestTestCase
class CVECheck(OESelftestTestCase):
def test_version_compare(self):
result = Version("100") > Version("99")
self.assertTrue( result, msg="Failed to compare version '100' > '99'")
result = Version("2.3.1") > Version("2.2.3")
self.assertTrue( result, msg="Failed to compare version '2.3.1' > '2.2.3'")
result = Version("2021-01-21") > Version("2020-12-25")
self.assertTrue( result, msg="Failed to compare version '2021-01-21' > '2020-12-25'")
result = Version("1.2-20200910") < Version("1.2-20200920")
self.assertTrue( result, msg="Failed to compare version '1.2-20200910' < '1.2-20200920'")
result = Version("1.0") >= Version("1.0beta")
self.assertTrue( result, msg="Failed to compare version '1.0' >= '1.0beta'")
result = Version("1.0-rc2") > Version("1.0-rc1")
self.assertTrue( result, msg="Failed to compare version '1.0-rc2' > '1.0-rc1'")
result = Version("1.0.alpha1") < Version("1.0")
self.assertTrue( result, msg="Failed to compare version '1.0.alpha1' < '1.0'")
result = Version("1.0_dev") <= Version("1.0")
self.assertTrue( result, msg="Failed to compare version '1.0_dev' <= '1.0'")
# ignore "p1" and "p2", so these should be equal
result = Version("1.0p2") <= Version("1.0p1") and Version("1.0p2") >= Version("1.0p1")
self.assertTrue( result ,msg="Failed to compare version '1.0p2' to '1.0p1'")