Skip to content

Commit

Permalink
feat: cyclonedx vex generation (intel#4150)
Browse files Browse the repository at this point in the history
  • Loading branch information
mastersans authored Jun 17, 2024
1 parent 129cce2 commit 4b87920
Show file tree
Hide file tree
Showing 6 changed files with 394 additions and 0 deletions.
2 changes: 2 additions & 0 deletions cve_bin_tool/vex_manager/__init__.py
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
147 changes: 147 additions & 0 deletions cve_bin_tool/vex_manager/generate.py
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
1 change: 1 addition & 0 deletions requirements.csv
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
118 changes: 118 additions & 0 deletions test/test_vex.py
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()
Loading

0 comments on commit 4b87920

Please sign in to comment.