Skip to content

feat: use cmp_version improved version comparisons #3430

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 10 additions & 77 deletions cve_bin_tool/cve_scanner.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
# Copyright (C) 2021 Intel Corporation
# SPDX-License-Identifier: GPL-3.0-or-later

import re
import sqlite3
import sys
from collections import defaultdict
from logging import Logger
from pathlib import Path
from string import ascii_lowercase
from typing import DefaultDict, Dict, List, Tuple
from typing import DefaultDict, Dict, List

from packaging.version import Version
from packaging.version import parse as parse_version
import cmp_version
from rich.console import Console

from cve_bin_tool.cvedb import DBNAME, DISK_LOCATION_DEFAULT
Expand Down Expand Up @@ -99,14 +97,12 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
# Removing * from vendors that are guessed by the package list parser
vendor = product_info.vendor.replace("*", "")

# Need to manipulate version to ensure canonical form of version

parsed_version, parsed_version_between = self.canonical_convert(product_info)
# If canonical form of version numbering not found, exit
if parsed_version == "UNKNOWN":
# Get a cmp_version.VersionString to do version compares
version_cmp = cmp_version.VersionString(product_info.version)
if version_cmp == "UNKNOWN":
return

self.cursor.execute(query, [vendor, product_info.product, str(parsed_version)])
self.cursor.execute(query, [vendor, product_info.product, str(version_cmp)])

cve_list = list(map(lambda x: x[0], self.cursor.fetchall()))

Expand All @@ -133,29 +129,17 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
version_end_excluding,
) = cve_range

# pep-440 doesn't include versions of the type 1.1.0g used by openssl
# or versions of the type 9a used by libjpeg
# so if this is openssl or libjpeg, convert the last letter to a .number
if product_info.product in {"openssl", "libjpeg"}:
# if last character is a letter, convert it to .number
# version = self.letter_convert(product_info.version)
version_start_including = self.letter_convert(version_start_including)
version_start_excluding = self.letter_convert(version_start_excluding)
version_end_including = self.letter_convert(version_end_including)
version_end_excluding = self.letter_convert(version_end_excluding)
parsed_version = parsed_version_between

# check the start range
passes_start = False
if (
version_start_including is not self.RANGE_UNSET
and parsed_version >= parse_version(version_start_including)
and version_cmp >= cmp_version.VersionString(version_start_including)
):
passes_start = True

if (
version_start_excluding is not self.RANGE_UNSET
and parsed_version > parse_version(version_start_excluding)
and version_cmp > cmp_version.VersionString(version_start_excluding)
):
passes_start = True

Expand All @@ -170,13 +154,13 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
passes_end = False
if (
version_end_including is not self.RANGE_UNSET
and parsed_version <= parse_version(version_end_including)
and version_cmp <= cmp_version.VersionString(version_end_including)
):
passes_end = True

if (
version_end_excluding is not self.RANGE_UNSET
and parsed_version < parse_version(version_end_excluding)
and version_cmp < cmp_version.VersionString(version_end_excluding)
):
passes_end = True

Expand Down Expand Up @@ -313,57 +297,6 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
if product_info not in self.all_product_data:
self.all_product_data[product_info] = len(cves)

def letter_convert(self, version: str) -> str:
"""pkg_resources follows pep-440 which doesn't expect openssl style 1.1.0g version numbering
or libjpeg style 9a version numbering
So to fake it, if the last character is a letter, replace it with .number before comparing
"""
if not version: # if version is empty return it.
return version

# Check for short string
if len(version) < 2:
return version

last_char = version[-1]
second_last_char = version[-2]

if last_char in self.ALPHA_TO_NUM and second_last_char in self.ALPHA_TO_NUM:
version = f"{version[:-2]}.{self.ALPHA_TO_NUM[second_last_char]}.{self.ALPHA_TO_NUM[last_char]}"

elif last_char in self.ALPHA_TO_NUM:
version = f"{version[:-1]}.{self.ALPHA_TO_NUM[last_char]}"
return version

VersionType = Version

def canonical_convert(
self, product_info: ProductInfo
) -> Tuple[VersionType, VersionType]:
version_between = parse_version("")
if product_info.version == "":
return parse_version(product_info.version), version_between
if product_info.product in {"openssl", "libjpeg"}:
pv = re.search(r"\d[.\d]*[a-z]?", product_info.version)
version_between = parse_version(self.letter_convert(pv.group(0)))
else:
# Ensure canonical form of version numbering
if ":" in product_info.version:
# Handle x:a.b<string> e.g. 2:7.4+23
components = product_info.version.split(":")
pv = re.search(r"\d[.\d]*", components[1])
else:
# Handle a.b.c<string> e.g. 1.20.9rel1
pv = re.search(r"\d[.\d]*", product_info.version)
if pv is None:
parsed_version = "UNKNOWN"
self.logger.warning(
f"error parsing {product_info.vendor}.{product_info.product} v{product_info.version} - manual inspection required"
)
else:
parsed_version = parse_version(pv.group(0))
return parsed_version, version_between

def affected(self):
"""Returns list of vendor.product and version tuples identified from
scan"""
Expand Down
1 change: 1 addition & 0 deletions requirements.csv
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,4 @@ python_not_in_db,importlib_resources
vsajip_not_in_db,python-gnupg
anthonyharrison_not_in_db,lib4sbom
the_purl_authors_not_in_db,packageurl-python
python_not_in_db,cmp_version
3 changes: 2 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
aiohttp[speedups]>=3.7.4
beautifulsoup4
cmp_version
cvss
defusedxml
distro
Expand All @@ -11,7 +12,7 @@ jsonschema>=3.0.2
lib4sbom>=0.5.0
python-gnupg
packageurl-python
packaging<22.0
packaging
plotly
pyyaml>=5.4
requests
Expand Down
2 changes: 1 addition & 1 deletion test/test_csv2cve.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ async def test_csv2cve_valid_file(self, caplog):
) in caplog.record_tuples

for cve_count, product in [
[60, "haxx.curl version 7.34.0"],
[40, "haxx.curl version 7.34.0"],
[10, "mit.kerberos_5 version 1.15.1"],
]:
retrieved_cve_count = 0
Expand Down
53 changes: 0 additions & 53 deletions test/test_cvescanner.py

This file was deleted.