From ac108c6b3b6338db10e6374bb9113c7e657c4ffc Mon Sep 17 00:00:00 2001 From: Fabrice Fontaine Date: Wed, 16 Aug 2023 12:06:53 +0200 Subject: [PATCH] feat: add CPE summary CPE summary table is used to display the number of CVEs by product as well as 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 | 62 +++++++++++++++++++++++++- cve_bin_tool/output_engine/util.py | 36 +++++++++++++++ test/test_output_engine.py | 3 ++ 5 files changed, 104 insertions(+), 1 deletion(-) diff --git a/cve_bin_tool/cli.py b/cve_bin_tool/cli.py index 6cd0106895..45385db5b0 100644 --- a/cve_bin_tool/cli.py +++ b/cve_bin_tool/cli.py @@ -991,6 +991,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 ab2fc15baa..11e9707274 100644 --- a/cve_bin_tool/output_engine/__init__.py +++ b/cve_bin_tool/output_engine/__init__.py @@ -552,6 +552,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 @@ -577,6 +578,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 @@ -633,6 +635,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 e442c50d1c..d2b3b056ab 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""" @@ -99,6 +105,60 @@ def _output_console_nowrap( console.print(Panel("CVE SUMMARY", expand=False)) console.print(table) + # Create table instance for CPE Summary + table = Table() + # Add Head Columns to the Table + 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") + table.add_column("LOW CVEs Count") + table.add_column("UNKNWON CVEs Count") + table.add_column("TOTAL CVEs Count") + if all_product_data is not None: + for product_data in all_product_data: + color = None + summary = get_cve_summary( + {product_data: all_cve_data[product_data]}, exploits + ) + + # Display package with the color of the highest CVE + for severity, count in summary.items(): + if color is None and count > 0: + 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: + color = summary_color[severity.split("-")[0]] + else: + color = "white" + cells += [ + Text.styled(str(count), color), + ] + cells += [ + Text.styled(str(all_product_data[product_data]), color), + ] + table.add_row(*cells) + # Print the table to the console + console.print(Panel("CPE SUMMARY", expand=False)) + console.print(table) + cve_by_remarks: defaultdict[Remarks, list[dict[str, str]]] = defaultdict(list) cve_by_paths: defaultdict[Remarks, list[dict[str, str]]] = defaultdict(list) # group cve_data by its remarks and separately by paths diff --git a/cve_bin_tool/output_engine/util.py b/cve_bin_tool/output_engine/util.py index a2411e0c57..b6d81c6930 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,40 @@ 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:" + + 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, + ) + 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 0e6aadec55..e72ef3fdce 100644 --- a/test/test_output_engine.py +++ b/test/test_output_engine.py @@ -891,6 +891,7 @@ def test_output_console(self): affected_versions, exploits, all_product_data, + True, console, outfile, ) @@ -924,6 +925,7 @@ def test_output_console_affected_versions(self): affected_versions, exploits, all_product_data, + True, console, outfile, ) @@ -970,6 +972,7 @@ def test_output_console_outfile(self): affected_versions, exploits, all_product_data, + True, outfile, )