diff --git a/README.md b/README.md index 73cb18e4fd..6606a2e8d3 100644 --- a/README.md +++ b/README.md @@ -508,6 +508,7 @@ Output: specify type of software bill of materials (sbom) to generate (default: spdx) --sbom-format {tag,json,yaml} specify format of software bill of materials (sbom) to generate (default: tag) + --strip-scan-dir strip scan directory from sbom evidence location paths and CVE paths (useful with a firmware dump) Vex Output: Arguments related to Vex output document. diff --git a/cve_bin_tool/cli.py b/cve_bin_tool/cli.py index bf93b0e483..f1bfd7342e 100644 --- a/cve_bin_tool/cli.py +++ b/cve_bin_tool/cli.py @@ -348,9 +348,9 @@ def main(argv=None): help="specify format of software bill of materials (sbom) to generate (default: tag)", ) output_group.add_argument( - "--sbom-strip-root", + "--strip-scan-dir", action="store_true", - help="strip SBOM root from evidence location paths (useful when building SBOM from firmware dump)", + help="strip scan directory from sbom evidence location paths and CVE paths (useful with a firmware dump)", default=False, ) vex_output_group = parser.add_argument_group( @@ -1250,7 +1250,7 @@ def main(argv=None): sbom_type=args["sbom_type"], sbom_format=args["sbom_format"], sbom_root=sbom_root, - sbom_strip_root=args["sbom_strip_root"], + strip_scan_dir=args["strip_scan_dir"], offline=args["offline"], ) diff --git a/cve_bin_tool/output_engine/__init__.py b/cve_bin_tool/output_engine/__init__.py index 8af9a2d110..eeccc3ba75 100644 --- a/cve_bin_tool/output_engine/__init__.py +++ b/cve_bin_tool/output_engine/__init__.py @@ -32,6 +32,7 @@ intermediate_output, ) from cve_bin_tool.sbom_manager.generate import SBOMGenerate +from cve_bin_tool.util import strip_path from cve_bin_tool.version import VERSION from cve_bin_tool.vex_manager.generate import VEXGenerate @@ -44,6 +45,7 @@ def save_intermediate( products_with_cve: int, products_without_cve: int, total_files: int, + strip_scan_dir: bool = False, ): """Save the intermediate report""" @@ -54,6 +56,7 @@ def save_intermediate( products_with_cve, products_without_cve, total_files, + strip_scan_dir, ) with open(filename, "w") as f: json.dump(inter_output, f, indent=" ") @@ -62,14 +65,18 @@ def save_intermediate( def output_csv( all_cve_data: dict[ProductInfo, CVEData], all_cve_version_info: dict[str, VersionInfo] | None, + scanned_dir: str, outfile, detailed: bool = False, affected_versions: int = 0, metrics: bool = False, + strip_scan_dir: bool = False, ): """Output a CSV of CVEs""" formatted_output = format_output( all_cve_data, + scanned_dir, + strip_scan_dir, all_cve_version_info, detailed, affected_versions, @@ -123,11 +130,13 @@ def output_pdf( is_report, products_with_cve, all_cve_version_info, + scanned_dir: str, outfile, merge_report, affected_versions: int = 0, exploits: bool = False, metrics: bool = False, + strip_scan_dir: bool = False, all_product_data=None, ): """Output a PDF of CVEs""" @@ -321,7 +330,15 @@ def output_pdf( "comments": cve.comments, } ) - path_elements = ", ".join(cve_data["paths"]) + if strip_scan_dir: + path_elements = ", ".join( + [ + strip_path(path, scanned_dir) + for path in cve_data["paths"] + ] + ) + else: + path_elements = ", ".join(cve_data["paths"]) for path_element in path_elements.split(","): path_entry = { "vendor": product_info.vendor, @@ -589,11 +606,13 @@ def output_pdf( is_report, products_with_cve, all_cve_version_info, + scanned_dir: str, outfile, merge_report, affected_versions: int = 0, exploits: bool = False, all_product_data=None, + strip_scan_dir: bool = False, ): """Output a PDF of CVEs Required module: Reportlab not found""" @@ -629,6 +648,7 @@ class OutputEngine: sbom_type (str) sbom_format (str) sbom_root (str) + strip_scan_dir (bool) offline (bool) Methods: @@ -667,7 +687,7 @@ def __init__( sbom_type: str = "spdx", sbom_format: str = "tag", sbom_root: str = "CVE_SBOM", - sbom_strip_root: bool = False, + strip_scan_dir: bool = False, vex_filename: str = "", vex_type: str = "", vex_product_info: dict[str, str] = {}, @@ -699,7 +719,7 @@ def __init__( self.sbom_type = sbom_type self.sbom_format = sbom_format self.sbom_root = sbom_root - self.sbom_strip_root = sbom_strip_root + self.strip_scan_dir = strip_scan_dir self.offline = offline self.organized_arguements = organized_arguements self.sbom_packages = {} @@ -716,15 +736,18 @@ def output_cves(self, outfile, output_type="console"): output_json( self.all_cve_data, self.all_cve_version_info, + self.scanned_dir, outfile, self.detailed, self.affected_versions, self.metrics, + self.strip_scan_dir, ) elif output_type == "json2": output_json2( self.all_cve_data, self.all_cve_version_info, + self.scanned_dir, self.time_of_last_update, outfile, self.affected_versions, @@ -732,15 +755,18 @@ def output_cves(self, outfile, output_type="console"): self.detailed, self.exploits, self.metrics, + self.strip_scan_dir, ) elif output_type == "csv": output_csv( self.all_cve_data, self.all_cve_version_info, + self.scanned_dir, outfile, self.detailed, self.affected_versions, self.metrics, + self.strip_scan_dir, ) elif output_type == "pdf": output_pdf( @@ -748,11 +774,13 @@ def output_cves(self, outfile, output_type="console"): self.is_report, self.products_with_cve, self.all_cve_version_info, + self.scanned_dir, outfile, self.merge_report, self.affected_versions, self.exploits, self.metrics, + self.strip_scan_dir, ) elif output_type == "html": output_html( @@ -768,15 +796,18 @@ def output_cves(self, outfile, output_type="console"): self.logger, outfile, self.affected_versions, + self.strip_scan_dir, ) else: # console, or anything else that is unrecognised output_console( self.all_cve_data, self.all_cve_version_info, + self.scanned_dir, self.time_of_last_update, self.affected_versions, self.exploits, self.metrics, + self.strip_scan_dir, self.all_product_data, self.offline, None, @@ -789,6 +820,7 @@ def output_cves(self, outfile, output_type="console"): self.append, self.tag, self.scanned_dir, + self.strip_scan_dir, self.products_with_cve, self.products_without_cve, self.total_files, @@ -819,7 +851,7 @@ def output_cves(self, outfile, output_type="console"): self.sbom_type, self.sbom_format, self.sbom_root, - self.sbom_strip_root, + self.strip_scan_dir, self.logger, ) sbomgen.generate_sbom() diff --git a/cve_bin_tool/output_engine/console.py b/cve_bin_tool/output_engine/console.py index f12d674c93..fc28153d96 100644 --- a/cve_bin_tool/output_engine/console.py +++ b/cve_bin_tool/output_engine/console.py @@ -18,7 +18,7 @@ from ..input_engine import Remarks from ..linkify import linkify_cve from ..theme import cve_theme -from ..util import ProductInfo, VersionInfo +from ..util import ProductInfo, VersionInfo, strip_path from ..version import VERSION from .util import ( format_path, @@ -47,10 +47,12 @@ def output_console(*args: Any): def _output_console_nowrap( all_cve_data: dict[ProductInfo, CVEData], all_cve_version_info: dict[str, VersionInfo], + scanned_dir: str, time_of_last_update: datetime, affected_versions: int, exploits: bool = False, metrics: bool = False, + strip_scan_dir: bool = False, all_product_data=None, offline: bool = False, width: int = None, @@ -286,13 +288,17 @@ def validate_cell_length(cell_name, cell_type): color = "green" for cve_data in cve_by_paths[remarks]: path_root = format_path(cve_data["paths"]) + if strip_scan_dir: + path_root_0 = strip_path(path_root[0], scanned_dir) + else: + path_root_0 = path_root[0] cells = [ Text.styled(validate_cell_length(cve_data["vendor"], "Vendor "), color), Text.styled( validate_cell_length(cve_data["product"], "Product "), color ), Text.styled(cve_data["version"], color), - Text.styled(validate_cell_length(path_root[0], "Root "), color), + Text.styled(validate_cell_length(path_root_0, "Root "), color), Text.styled(validate_cell_length(path_root[1], "Path "), color), ] table.add_row(*cells) diff --git a/cve_bin_tool/output_engine/html.py b/cve_bin_tool/output_engine/html.py index 22ff250718..06b72a0ecb 100644 --- a/cve_bin_tool/output_engine/html.py +++ b/cve_bin_tool/output_engine/html.py @@ -14,7 +14,7 @@ from cve_bin_tool.merge import MergeReports -from ..util import CVEData, ProductInfo, Remarks, VersionInfo +from ..util import CVEData, ProductInfo, Remarks, VersionInfo, strip_path from ..version import VERSION from .print_mode import html_print_mode from .util import group_cve_by_remark @@ -96,6 +96,7 @@ def output_html( logger: Logger, outfile, affected_versions: int = 0, + strip_scan_dir: bool = False, ): """Returns a HTML report for CVE's""" @@ -403,6 +404,13 @@ def output_html( if not_affected_cves: remarks += "not_affected " + if strip_scan_dir: + product_paths = [ + strip_path(path, scanned_dir) for path in cve_data["paths"] + ] + else: + product_paths = cve_data["paths"] + products_found.append( product_row.render( vendor=product_info.vendor, @@ -418,8 +426,8 @@ def output_html( ), remarks=remarks, fix_id=hid, - paths=cve_data["paths"], - len_paths=len(cve_data["paths"]), + paths=product_paths, + len_paths=len(product_paths), new_cves=new_cves, mitigated_cves=mitigated_cves, confirmed_cves=confirmed_cves, @@ -433,7 +441,7 @@ def output_html( star_warn = "* vendors guessed by the tool" # update all_paths - for path in cve_data["paths"]: + for path in product_paths: all_paths[path].append(hid) # Dashboard Rendering diff --git a/cve_bin_tool/output_engine/json_output.py b/cve_bin_tool/output_engine/json_output.py index 6bd80059db..856364b7e2 100644 --- a/cve_bin_tool/output_engine/json_output.py +++ b/cve_bin_tool/output_engine/json_output.py @@ -15,7 +15,14 @@ def vulnerabilities_builder( - all_cve_data, exploits, all_cve_version_info, detailed, affected_versions, metrics + all_cve_data, + exploits, + all_cve_version_info, + scanned_dir, + strip_scan_dir, + detailed, + affected_versions, + metrics, ): """ Builds a dictionary of vulnerabilities based on the provided inputs. @@ -25,7 +32,13 @@ def vulnerabilities_builder( vulnerability_reports = [] source_entries_map = {} formatted_cve_data = format_output( - all_cve_data, all_cve_version_info, detailed, affected_versions, metrics + all_cve_data, + scanned_dir, + strip_scan_dir, + all_cve_version_info, + detailed, + affected_versions, + metrics, ) for cve_entry in formatted_cve_data: source = cve_entry["source"] @@ -84,14 +97,22 @@ def metadata_builder(organized_parameters): def output_json( all_cve_data: dict[ProductInfo, CVEData], all_cve_version_info: dict[str, VersionInfo], + scanned_dir: str, outfile: IO, detailed: bool = False, affected_versions: int = 0, metrics: bool = False, + strip_scan_dir: bool = False, ): """Output a JSON of CVEs""" formatted_output = format_output( - all_cve_data, all_cve_version_info, detailed, affected_versions, metrics + all_cve_data, + scanned_dir, + strip_scan_dir, + all_cve_version_info, + detailed, + affected_versions, + metrics, ) json.dump(formatted_output, outfile, indent=2) @@ -99,6 +120,7 @@ def output_json( def output_json2( all_cve_data: dict[ProductInfo, CVEData], all_cve_version_info: dict[str, VersionInfo], + scanned_dir: str, time_of_last_update: datetime, outfile: IO, affected_versions: int, @@ -106,6 +128,7 @@ def output_json2( detailed: bool = False, exploits: bool = False, metrics: bool = False, + strip_scan_dir: bool = False, ): """Output a JSON of CVEs in JSON2 format""" output = {} @@ -119,8 +142,10 @@ def output_json2( all_cve_data, exploits, all_cve_version_info, + scanned_dir, detailed, affected_versions, metrics, + strip_scan_dir, ) json.dump(output, outfile, indent=2) diff --git a/cve_bin_tool/output_engine/util.py b/cve_bin_tool/output_engine/util.py index fcbe5cc41b..3ded2d86fc 100644 --- a/cve_bin_tool/output_engine/util.py +++ b/cve_bin_tool/output_engine/util.py @@ -13,7 +13,7 @@ from cve_bin_tool.util import make_http_requests -from ..util import CVE, CVEData, ProductInfo, Remarks, VersionInfo +from ..util import CVE, CVEData, ProductInfo, Remarks, VersionInfo, strip_path def get_cve_summary( @@ -145,6 +145,8 @@ def format_version_range(version_info: VersionInfo) -> str: def format_output( all_cve_data: dict[ProductInfo, CVEData], + scanned_dir: str, + strip_scan_dir: bool = False, all_cve_version_info: dict[str, VersionInfo] | None = None, detailed: bool = False, affected_versions: int = 0, @@ -187,6 +189,12 @@ def format_output( if metric == "EPSS": probability = round(field[0], 5) percentile = field[1] + if strip_scan_dir: + paths = ", ".join( + [strip_path(path, scanned_dir) for path in cve_data["paths"]] + ) + else: + paths = ", ".join(cve_data["paths"]) details = { "vendor": product_info.vendor, "product": product_info.product, @@ -197,7 +205,7 @@ def format_output( "source": cve.data_source, "cvss_version": str(cve.cvss_version), "cvss_vector": cve.cvss_vector, - "paths": ", ".join(cve_data["paths"]), + "paths": paths, "remarks": cve.remarks.name, "comments": cve.comments, } @@ -231,6 +239,7 @@ def intermediate_output( products_with_cve: int, products_without_cve: int, total_files: int, + strip_scan_dir: bool = False, ) -> dict[dict[str, str | int], list[dict[str, str]]]: """ summary: Generate an intermediate output in the list of dictionary format with some metadata. @@ -267,7 +276,7 @@ def intermediate_output( "products_without_cve": products_without_cve, "total_files": total_files, }, - "report": format_output(all_cve_data), + "report": format_output(all_cve_data, scanned_dir, strip_scan_dir), } diff --git a/cve_bin_tool/sbom_manager/generate.py b/cve_bin_tool/sbom_manager/generate.py index ba80dc698b..a3dad6c9fc 100644 --- a/cve_bin_tool/sbom_manager/generate.py +++ b/cve_bin_tool/sbom_manager/generate.py @@ -1,7 +1,6 @@ # Copyright (C) 2024 Intel Corporation # SPDX-License-Identifier: GPL-3.0-or-later -import os from logging import Logger from pathlib import Path from typing import Optional @@ -12,6 +11,7 @@ from lib4sbom.sbom import SBOM from cve_bin_tool.log import LOGGER +from cve_bin_tool.util import strip_path from cve_bin_tool.version import VERSION @@ -31,7 +31,7 @@ def __init__( sbom_type="spdx", sbom_format="tag", sbom_root="CVE-SCAN", - sbom_strip_root=False, + strip_scan_dir=False, logger: Optional[Logger] = None, ): self.all_product_data = all_product_data @@ -40,7 +40,7 @@ def __init__( self.sbom_type = sbom_type self.sbom_format = sbom_format self.sbom_root = sbom_root - self.sbom_strip_root = sbom_strip_root + self.strip_scan_dir = strip_scan_dir self.logger = logger or LOGGER.getChild(self.__class__.__name__) self.sbom_packages = {} @@ -100,11 +100,8 @@ def generate_sbom(self) -> None: ): if self.all_cve_data.get(product_data): for path in self.all_cve_data[product_data]["paths"]: - if self.sbom_strip_root: - drive, root, tail = os.path.splitroot(path) - evidence = ( - drive + root + os.path.relpath(path, self.sbom_root) - ) + if self.strip_scan_dir: + evidence = strip_path(path, self.sbom_root) else: evidence = path my_package.set_evidence(evidence) diff --git a/cve_bin_tool/util.py b/cve_bin_tool/util.py index 5456e2186c..beb9d11fd7 100644 --- a/cve_bin_tool/util.py +++ b/cve_bin_tool/util.py @@ -608,3 +608,8 @@ def decode_cpe22(cpe22) -> list: def windows_fixup(filename): """Replace colon and backslash in filename to avoid a failure on Windows""" return filename.replace(":", "_").replace("\\", "_") + + +def strip_path(path_element: str, scanned_dir: str) -> str: + path = Path(path_element) + return path.drive + path.root + os.path.relpath(path_element, scanned_dir) diff --git a/test/test_output_engine.py b/test/test_output_engine.py index 06a6bfb80a..74b7861a31 100644 --- a/test/test_output_engine.py +++ b/test/test_output_engine.py @@ -1045,7 +1045,7 @@ def test_formatted_detailed_output(self): def test_output_json(self): """Test formatting output as JSON""" - output_json(self.MOCK_OUTPUT, None, self.mock_file, metrics=True) + output_json(self.MOCK_OUTPUT, None, ".", self.mock_file, metrics=True) self.mock_file.seek(0) # reset file position self.assertEqual(json.load(self.mock_file), self.FORMATTED_OUTPUT) @@ -1054,6 +1054,7 @@ def test_output_json2(self): output_json2( self.MOCK_OUTPUT, None, + ".", datetime.today(), self.mock_file, 0, @@ -1081,7 +1082,7 @@ def test_output_json2(self): def test_output_csv(self): """Test formatting output as CSV""" - output_csv(self.MOCK_OUTPUT, None, self.mock_file, metrics=True) + output_csv(self.MOCK_OUTPUT, None, ".", self.mock_file, metrics=True) self.mock_file.seek(0) # reset file position reader = csv.DictReader(self.mock_file) actual_value = [dict(x) for x in reader] @@ -1105,7 +1106,15 @@ def test_output_pdf(self): import pdftotext output_pdf( - self.MOCK_PDF_OUTPUT, False, 1, None, "cve_test.pdf", False, 0, metrics=True + self.MOCK_PDF_OUTPUT, + False, + ".", + 1, + None, + "cve_test.pdf", + False, + 0, + metrics=True, ) with open("cve_test.pdf", "rb") as f: pdf = pdftotext.PDF(f, physical=True) @@ -1148,10 +1157,12 @@ def test_output_console(self): output_console( self.MOCK_OUTPUT, self.MOCK_ALL_CVE_VERSION_INFO, + ".", time_of_last_update, affected_versions, exploits, metrics, + False, all_product_data, True, 120, @@ -1197,10 +1208,12 @@ def test_output_console_affected_versions(self): output_console( self.MOCK_ALL_CVE_DATA, self.MOCK_ALL_CVE_VERSION_INFO, + ".", time_of_last_update, affected_versions, exploits, metrics, + False, all_product_data, True, 120, @@ -1247,10 +1260,12 @@ def test_output_console_outfile(self): output_console( self.MOCK_OUTPUT, self.MOCK_ALL_CVE_VERSION_INFO, + ".", time_of_last_update, affected_versions, exploits, metrics, + False, all_product_data, True, 120, @@ -1296,10 +1311,12 @@ def test_output_console_metrics_false(self): output_console( self.MOCK_OUTPUT_2, self.MOCK_ALL_CVE_VERSION_INFO, + ".", time_of_last_update, affected_versions, exploits, metrics, + False, all_product_data, True, 120, @@ -1505,7 +1522,7 @@ def test_csv_macros(self): }, ] - output_csv(bad_input, None, self.mock_file, metrics=True) + output_csv(bad_input, None, ".", self.mock_file, metrics=True) self.mock_file.seek(0) # reset file position reader = csv.DictReader(self.mock_file) actual_output = [dict(x) for x in reader] diff --git a/test/test_output_engine_init.py b/test/test_output_engine_init.py index 5cd6c961b1..a899743255 100644 --- a/test/test_output_engine_init.py +++ b/test/test_output_engine_init.py @@ -33,7 +33,7 @@ def __init__(self): def dummy_intermediate_output( - all_cve_data, tag, scanned_dir, with_cve, without_cve, total_files + all_cve_data, tag, scanned_dir, with_cve, without_cve, total_files, strip_scan_dir ): # Minimal dummy report structure return {