diff --git a/cve_bin_tool/vex_manager/__init__.py b/cve_bin_tool/vex_manager/__init__.py new file mode 100644 index 0000000000..6dc51c1f57 --- /dev/null +++ b/cve_bin_tool/vex_manager/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/cve_bin_tool/vex_manager/generate.py b/cve_bin_tool/vex_manager/generate.py new file mode 100644 index 0000000000..01bfb53704 --- /dev/null +++ b/cve_bin_tool/vex_manager/generate.py @@ -0,0 +1,147 @@ +# Copyright (C) 2024 Intel Corporation +# SPDX-License-Identifier: GPL-3.0-or-later +import os +from datetime import datetime +from logging import Logger +from pathlib import Path +from typing import Dict, List, Optional + +from lib4sbom.data.vulnerability import Vulnerability +from lib4vex.generator import VEXGenerator + +from cve_bin_tool.log import LOGGER +from cve_bin_tool.util import CVEData, ProductInfo, Remarks + + +class VEXGenerate: + analysis_state = { + "cyclonedx": { + Remarks.NewFound: "in_triage", + Remarks.Unexplored: "in_triage", + Remarks.Confirmed: "exploitable", + Remarks.Mitigated: "resolved", + Remarks.FalsePositive: "false_positive", + Remarks.NotAffected: "not_affected", + }, + "csaf": { + Remarks.NewFound: "under_investigation", + Remarks.Unexplored: "under_investigation", + Remarks.Confirmed: "known_affected", + Remarks.Mitigated: "fixed", + Remarks.FalsePositive: "known_not_affected", + Remarks.NotAffected: "known_not_affected", + }, + } + + def __init__( + self, + product: str, + release: str, + vendor: str, + filename: str, + vextype: str, + all_cve_data: Dict[ProductInfo, CVEData], + sbom: Optional[str] = None, + logger: Optional[Logger] = None, + validate: bool = True, + ): + self.product = product + self.release = release + self.vendor = vendor + self.sbom = sbom + self.filename = filename + self.vextype = vextype + self.logger = logger or LOGGER.getChild(self.__class__.__name__) + self.validate = validate + self.all_cve_data = all_cve_data + + def generate_vex(self) -> None: + """ + Generates VEX code based on the specified VEX type. + + Returns: + None + """ + vexgen = VEXGenerator(vex_type=self.vextype) + kwargs = {"name": self.product, "release": self.release} + if self.sbom: + kwargs["sbom"] = self.sbom + vexgen.set_product(**kwargs) + if Path(self.filename).is_file(): + self.logger.warning( + f"Failed to write '{self.filename}'. File already exists" + ) + self.logger.info("Generating a new filename with Default Naming Convention") + self.filename = self.generate_vex_filename() + vexgen.generate( + project_name=self.product, + vex_data=self.get_vulnerabilities(), + metadata=self.get_metadata(), + filename=self.filename, + ) + + def generate_vex_filename(self) -> str: + """ + Generates a VEX filename based on the current date and time. + + Returns: + str: The generated VEX filename. + """ + now = datetime.now().strftime("%Y-%m-%d.%H-%M-%S") + filename = os.path.abspath( + os.path.join( + os.getcwd(), f"{self.product}_{self.release}_{self.vextype}.{now}.json" + ) + ) + return filename + + def get_metadata(self) -> Dict: + metadata = { + "id": f"{self.product.upper()}-{self.release}-VEX", + "supplier": self.vendor, + } + # other metadata can be added here + return metadata + + def get_vulnerabilities(self) -> List[Vulnerability]: + """ + Retrieves a list of vulnerabilities. + + Returns: + A list of Vulnerability objects representing the vulnerabilities. + """ + vulnerabilities = [] + for product_info, cve_data in self.all_cve_data.items(): + vendor, product, version, _, purl = product_info + for cve in cve_data["cves"]: + if isinstance(cve, str): + continue + vulnerability = Vulnerability(validation=self.vextype) + vulnerability.initialise() + vulnerability.set_name(product) + vulnerability.set_release(version) + vulnerability.set_id(cve.cve_number) + vulnerability.set_description(cve.description) + vulnerability.set_comment(cve.comments) + vulnerability.set_status(self.analysis_state[self.vextype][cve.remarks]) + if cve.justification: + vulnerability.set_justification(cve.justification) + # vulnerability.set_remediation(cve.response) + detail = ( + f"{cve.remarks.name}: {cve.comments}" + if cve.comments + else cve.remarks.name + ) + # more details will be added using set_value() + bom_version = 1 + ref = f"urn:cbt:{bom_version}/{vendor}#{product}:{version}" + if purl: + vulnerability.set_value("purl", purl) + vulnerability.set_value("bom_link", ref) + vulnerability.set_value("action", detail) + vulnerability.set_value("source", cve.data_source) + vulnerability.set_value("updated", cve.last_modified) + # vulnerability.show_vulnerability() + vulnerabilities.append(vulnerability.get_vulnerability()) + self.logger.debug(f"Vulnerabilities: {vulnerabilities}") + return vulnerabilities diff --git a/requirements.csv b/requirements.csv index 3ac40cc600..763e346f29 100644 --- a/requirements.csv +++ b/requirements.csv @@ -21,5 +21,6 @@ python_not_in_db,packaging python_not_in_db,importlib_resources vsajip_not_in_db,python-gnupg anthonyharrison_not_in_db,lib4sbom +anthonyharrison_not_in_db,lib4vex the_purl_authors_not_in_db,packageurl-python h2non,filetype diff --git a/requirements.txt b/requirements.txt index 931bea13ee..5ceb406231 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,6 +10,7 @@ importlib_resources; python_version < "3.9" jinja2>=2.11.3 jsonschema>=3.0.2 lib4sbom>=0.7.0 +lib4vex>=0.1.0 python-gnupg packageurl-python packaging diff --git a/test/test_vex.py b/test/test_vex.py new file mode 100644 index 0000000000..120c62ae69 --- /dev/null +++ b/test/test_vex.py @@ -0,0 +1,118 @@ +# Copyright (C) 2021 Intel Corporation +# SPDX-License-Identifier: GPL-3.0-or-later +import json +import unittest +from pathlib import Path + +from cve_bin_tool.util import CVE, CVEData, ProductInfo, Remarks +from cve_bin_tool.vex_manager.generate import VEXGenerate + +TEST_DIR = Path(__file__).parent.resolve() +VEX_PATH = TEST_DIR / "vex" + + +class TestVexGeneration(unittest.TestCase): + FORMATTED_DATA = { + ProductInfo("vendor0", "product0", "1.0", "/usr/local/bin/product"): CVEData( + cves=[ + CVE( + "CVE-1234-1004", + "CRITICAL", + score=4.2, + cvss_version=2, + cvss_vector="C:H", + data_source="NVD", + last_modified="01-05-2019", + metric={ + "EPSS": [0.00126, "0.46387"], + }, + ), + CVE( + "CVE-1234-1005", + "MEDIUM", + remarks=Remarks.NotAffected, + comments="Detail field populated.", + score=4.2, + cvss_version=2, + cvss_vector="C:H", + data_source="NVD", + last_modified="01-05-2019", + metric={ + "EPSS": [0.00126, "0.46387"], + }, + justification="code_not_reachable", + response=["will_not_fix"], + ), + ], + paths={""}, + ), + ProductInfo("vendor0", "product0", "2.8.6", "/usr/local/bin/product"): CVEData( + cves=[ + CVE( + "CVE-1234-1007", + "LOW", + remarks=Remarks.Mitigated, + comments="Data field populated.", + score=2.5, + cvss_version=3, + cvss_vector="CVSS3.0/C:H/I:L/A:M", + data_source="NVD", + last_modified="12-12-2020", + metric={ + "EPSS": [0.03895, "0.37350"], + }, + ), + CVE( + "CVE-1234-1008", + "UNKNOWN", + score=2.5, + cvss_version=3, + cvss_vector="CVSS3.0/C:H/I:L/A:M", + data_source="NVD", + last_modified="12-12-2020", + metric={ + "EPSS": [0.03895, "0.37350"], + }, + ), + ], + paths={""}, + ), + } + + def test_output_cyclonedx(self): + """Test VEX output generation""" + + vexgen = VEXGenerate( + "dummy-product", + "1.0", + "dummy-vendor", + "generated_cyclonedx_vex.json", + "cyclonedx", + self.FORMATTED_DATA, + ) + vexgen.generate_vex() + with open("generated_cyclonedx_vex.json") as f: + json_data = json.load(f) + # remove timestamp and serialNumber from generated json as they are dynamic + json_data.get("metadata", {}).pop("timestamp", None) + json_data.pop("serialNumber", None) + for vulnerability in json_data.get("vulnerabilities", []): + vulnerability.pop("published", None) + vulnerability.pop("updated", None) + + with open(str(VEX_PATH / "test_cyclonedx_vex.json")) as f: + expected_json = json.load(f) + # remove timestamp and serialNumber from expected json as they are dynamic + expected_json.get("metadata", {}).pop("timestamp", None) + expected_json.pop("serialNumber", None) + for vulnerability in expected_json.get("vulnerabilities", []): + vulnerability.pop("published", None) + vulnerability.pop("updated", None) + + assert json_data == expected_json + + Path("generated_cyclonedx_vex.json").unlink() + + +if __name__ == "__main__": + unittest.main() diff --git a/test/vex/test_cyclonedx_vex.json b/test/vex/test_cyclonedx_vex.json new file mode 100644 index 0000000000..66a1183938 --- /dev/null +++ b/test/vex/test_cyclonedx_vex.json @@ -0,0 +1,125 @@ +{ + "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json", + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": "urn:uuid:9dbd4a19-324a-4ac6-b29b-37202d2bcd90", + "version": 1, + "metadata": { + "timestamp": "2024-06-14T22:39:24Z", + "tools": { + "components": [ + { + "name": "lib4vex", + "version": "0.1.0", + "type": "application" + } + ] + }, + "properties": [ + { + "name": "Revision_1", + "value": "Initial version" + } + ], + "component": { + "type": "application", + "bom-ref": "CDXRef-DOCUMENT", + "name": "dummy-product" + } + }, + "vulnerabilities": [ + { + "bom-ref": "product0@1.0", + "id": "CVE-1234-1004", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-1234-1004" + }, + "description": "", + "published": "2024-06-14T22:39:24Z", + "updated": "2024-06-14T22:39:24Z", + "analysis": { + "state": "in_triage", + "detail": "" + }, + "affects": [ + { + "ref": "urn:cbt:1/vendor0#product0:1.0", + "versions": { + "status": "unknown" + } + } + ] + }, + { + "bom-ref": "product0@1.0", + "id": "CVE-1234-1005", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-1234-1005" + }, + "description": "", + "published": "2024-06-14T22:39:24Z", + "updated": "2024-06-14T22:39:24Z", + "analysis": { + "state": "not_affected", + "detail": "Detail field populated.", + "justification": "code_not_reachable" + }, + "affects": [ + { + "ref": "urn:cbt:1/vendor0#product0:1.0", + "versions": { + "status": "unaffected" + } + } + ] + }, + { + "bom-ref": "product0@2.8.6", + "id": "CVE-1234-1007", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-1234-1007" + }, + "description": "", + "published": "2024-06-14T22:39:24Z", + "updated": "2024-06-14T22:39:24Z", + "analysis": { + "state": "resolved", + "detail": "Data field populated." + }, + "affects": [ + { + "ref": "urn:cbt:1/vendor0#product0:2.8.6", + "versions": { + "status": "affected" + } + } + ] + }, + { + "bom-ref": "product0@2.8.6", + "id": "CVE-1234-1008", + "source": { + "name": "NVD", + "url": "https://nvd.nist.gov/vuln/detail/CVE-1234-1008" + }, + "description": "", + "published": "2024-06-14T22:39:24Z", + "updated": "2024-06-14T22:39:24Z", + "analysis": { + "state": "in_triage", + "detail": "" + }, + "affects": [ + { + "ref": "urn:cbt:1/vendor0#product0:2.8.6", + "versions": { + "status": "unknown" + } + } + ] + } + ] +}