From 3b3e5a39f51c03e719adc824ac2ae67578524d5b Mon Sep 17 00:00:00 2001 From: Fabrice Fontaine Date: Wed, 16 Aug 2023 12:06:53 +0200 Subject: [PATCH] feat: add latest upstream stable version in CPE summary CPE summary table is updated to display the latest upstream stable version (retrieved thanks to release-monitoring project). This first iteration doesn't use a local cache and so only works in online mode. Signed-off-by: Fabrice Fontaine --- cve_bin_tool/cli.py | 1 + cve_bin_tool/output_engine/__init__.py | 3 ++ cve_bin_tool/output_engine/console.py | 16 ++++++++++- cve_bin_tool/output_engine/util.py | 40 ++++++++++++++++++++++++++ test/test_output_engine.py | 3 ++ 5 files changed, 62 insertions(+), 1 deletion(-) diff --git a/cve_bin_tool/cli.py b/cve_bin_tool/cli.py index 2a22e1511a..ec8257d24c 100644 --- a/cve_bin_tool/cli.py +++ b/cve_bin_tool/cli.py @@ -1005,6 +1005,7 @@ def main(argv=None): sbom_type=args["sbom_type"], sbom_format=args["sbom_format"], sbom_root=sbom_root, + offline=args["offline"], ) if not args["quiet"]: diff --git a/cve_bin_tool/output_engine/__init__.py b/cve_bin_tool/output_engine/__init__.py index 7f2c3d95b6..8add32277a 100644 --- a/cve_bin_tool/output_engine/__init__.py +++ b/cve_bin_tool/output_engine/__init__.py @@ -624,6 +624,7 @@ def __init__( sbom_type: str = "spdx", sbom_format: str = "tag", sbom_root: str = "CVE_SBOM", + offline: bool = False, ): self.logger = logger or LOGGER.getChild(self.__class__.__name__) self.all_cve_version_info = all_cve_version_info @@ -649,6 +650,7 @@ def __init__( self.sbom_type = sbom_type self.sbom_format = sbom_format self.sbom_root = sbom_root + self.offline = offline def output_cves(self, outfile, output_type="console"): """Output a list of CVEs @@ -705,6 +707,7 @@ def output_cves(self, outfile, output_type="console"): self.affected_versions, self.exploits, self.all_product_data, + self.offline, outfile, ) diff --git a/cve_bin_tool/output_engine/console.py b/cve_bin_tool/output_engine/console.py index d9bcc20a75..c01e9f4c44 100644 --- a/cve_bin_tool/output_engine/console.py +++ b/cve_bin_tool/output_engine/console.py @@ -20,7 +20,12 @@ from ..theme import cve_theme from ..util import ProductInfo, VersionInfo from ..version import VERSION -from .util import format_path, format_version_range, get_cve_summary +from .util import ( + format_path, + format_version_range, + get_cve_summary, + get_latest_upstream_stable_version, +) def output_console(*args: Any): @@ -46,6 +51,7 @@ def _output_console_nowrap( affected_versions: int, exploits: bool = False, all_product_data=None, + offline: bool = False, console: Console = Console(theme=cve_theme), ): """Output list of CVEs in a tabular format with color support""" @@ -105,6 +111,7 @@ def _output_console_nowrap( table.add_column("Vendor") table.add_column("Product") table.add_column("Version") + table.add_column("Latest Upstream Stable Version") table.add_column("CRITICAL CVEs Count") table.add_column("HIGH CVEs Count") table.add_column("MEDIUM CVEs Count") @@ -124,10 +131,17 @@ def _output_console_nowrap( color = summary_color[severity.split("-")[0]] if all_product_data[product_data] != 0: + if offline: + latest_stable_version = "UNKNOWN (offline mode)" + else: + latest_stable_version = get_latest_upstream_stable_version( + product_data + ) cells = [ Text.styled(product_data.vendor, color), Text.styled(product_data.product, color), Text.styled(product_data.version, color), + Text.styled(latest_stable_version, color), ] for severity, count in summary.items(): if count > 0: diff --git a/cve_bin_tool/output_engine/util.py b/cve_bin_tool/output_engine/util.py index 920e4dd678..02cef8e9cf 100644 --- a/cve_bin_tool/output_engine/util.py +++ b/cve_bin_tool/output_engine/util.py @@ -11,6 +11,8 @@ from collections import defaultdict from datetime import datetime +import requests + from ..util import CVE, CVEData, ProductInfo, Remarks, VersionInfo @@ -48,6 +50,44 @@ def get_cve_summary( return summary +def get_latest_upstream_stable_version(product_info: ProductInfo) -> str: + """ + summary: Retrieve latest upstream stable version from release-monitoring.org + + Args: + ProductInfo + + Returns: + Latest upstream stable version + """ + latest_stable_version = "UNKNOWN" + + # Special case to handle linux kernel prefix + if product_info.product == "linux_kernel": + cpe_id_prefix = "cpe:2.3:o:" + else: + cpe_id_prefix = "cpe:2.3:a:" + + try: + response = requests.get( + "https://release-monitoring.org/api/v2/packages/?distribution=CPE NVD NIST&name=" + + cpe_id_prefix + + product_info.vendor + + ":" + + product_info.product, + timeout=300, + ) + except requests.exceptions.SSLError: + return latest_stable_version + + response.raise_for_status() + jsonResponse = response.json() + if jsonResponse["total_items"] != 0: + latest_stable_version = jsonResponse["items"][0]["stable_version"] + + return latest_stable_version + + def generate_filename(extension: str, prefix: str = "output") -> str: """ summary: Generate a unique filename with extension provided. diff --git a/test/test_output_engine.py b/test/test_output_engine.py index ef08bf1c21..93a26bd2ab 100644 --- a/test/test_output_engine.py +++ b/test/test_output_engine.py @@ -941,6 +941,7 @@ def test_output_console(self): affected_versions, exploits, all_product_data, + True, console, outfile, ) @@ -982,6 +983,7 @@ def test_output_console_affected_versions(self): affected_versions, exploits, all_product_data, + True, console, outfile, ) @@ -1028,6 +1030,7 @@ def test_output_console_outfile(self): affected_versions, exploits, all_product_data, + True, outfile, )