forked from intel/cve-bin-tool
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: cyclonedx vex generation (intel#4150)
- Loading branch information
1 parent
129cce2
commit 4b87920
Showing
6 changed files
with
394 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
# Copyright (C) 2024 Intel Corporation | ||
# SPDX-License-Identifier: GPL-3.0-or-later |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() |
Oops, something went wrong.