Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 53 additions & 9 deletions cve_bin_tool/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,12 @@ def main(argv=None):
help="provide sbom filename",
default="",
)
input_group.add_argument(
"--vex-file",
action="store",
help="provide vulnerability exchange (vex) filename for triage processing",
default="",
)

output_group = parser.add_argument_group("Output")
output_group.add_argument(
Expand Down Expand Up @@ -323,12 +329,6 @@ def main(argv=None):
help="Lists versions of product affected by a given CVE (to facilitate upgrades)",
)

output_group.add_argument(
"--vex",
action="store",
help="Provide vulnerability exchange (vex) filename",
default="",
)
output_group.add_argument(
"--sbom-output",
action="store",
Expand All @@ -349,7 +349,37 @@ def main(argv=None):
choices=["tag", "json", "yaml"],
help="specify format of software bill of materials (sbom) to generate (default: tag)",
)

output_group.add_argument(
"--vex-output",
action="store",
help="Provide vulnerability exchange (vex) filename to generate",
default="",
)
output_group.add_argument(
"--vex-type",
action="store",
default="cyclonedx",
choices=["cyclonedx", "csaf", "openvex"],
help="specify type of vulnerability exchange (vex) to generate (default: cyclonedx)",
)
output_group.add_argument(
"--product",
action="store",
default="",
help="Product Name",
)
output_group.add_argument(
"--release",
action="store",
default="",
help="Release Version",
)
output_group.add_argument(
"--vendor",
action="store",
default="",
help="Vendor/Supplier of Product",
)
parser.add_argument(
"-e",
"--exclude",
Expand Down Expand Up @@ -1075,7 +1105,19 @@ def main(argv=None):
)
)
LOGGER.info(f"Known CVEs in {affected_string}:")

vex_product_info: dict[str, str] = {}
if args["vex_output"]:
if args["product"] and args["release"] and args["vendor"]:
vex_product_info = {
"product": args["product"],
"release": args["release"],
"vendor": args["vendor"],
}
else:
LOGGER.error(
"Please provide --product, --release and --vendor for VEX generation"
)
return ERROR_CODES[InsufficientArgs]
# Creates an Object for OutputEngine
output = OutputEngine(
all_cve_data=cve_scanner.all_cve_data,
Expand All @@ -1097,7 +1139,9 @@ def main(argv=None):
exploits=args["exploits"],
metrics=metrics,
detailed=args["detailed"],
vex_filename=args["vex"],
vex_filename=args["vex_output"],
vex_type=args["vex_type"],
vex_product_info=vex_product_info,
sbom_filename=args["sbom_output"],
sbom_type=args["sbom_type"],
sbom_format=args["sbom_format"],
Expand Down
5 changes: 3 additions & 2 deletions cve_bin_tool/config_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,9 @@ def config_generator(config_format, organized_arguments):
group_args["sbom-type"]["arg_value"] = None
group_args["sbom-format"]["arg_value"] = None
group_args["sbom-output"]["arg_value"] = None
if group_args["vex"]["arg_value"] == "":
group_args["vex"]["arg_value"] = None
if group_args["vex-output"]["arg_value"] == "":
group_args["vex-type"]["arg_value"] = None
group_args["vex-output"]["arg_value"] = None
f.write(f"{first_char}{group_title}{last_char}\n")
for arg_name, arg_value_help in group_args.items():
arg_value = arg_value_help["arg_value"]
Expand Down
126 changes: 18 additions & 108 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from datetime import datetime
from logging import Logger
from pathlib import Path
from typing import IO, Any
from typing import IO

from cve_bin_tool.cve_scanner import CVEData
from cve_bin_tool.cvedb import CVEDB
Expand All @@ -31,6 +31,7 @@
from cve_bin_tool.sbom_manager.generate import SBOMGenerate
from cve_bin_tool.util import ProductInfo, Remarks, VersionInfo
from cve_bin_tool.version import VERSION
from cve_bin_tool.vex_manager.generate import VEXGenerate


def output_json(
Expand Down Expand Up @@ -672,14 +673,16 @@ def __init__(
affected_versions: int = 0,
all_cve_version_info=None,
detailed: bool = False,
vex_filename: str = "",
exploits: bool = False,
metrics: bool = False,
all_product_data=None,
sbom_filename: str = "",
sbom_type: str = "spdx",
sbom_format: str = "tag",
sbom_root: str = "CVE_SBOM",
vex_filename: str = "",
vex_type: str = "cyclonedx",
vex_product_info: dict[str, str] = {},
offline: bool = False,
):
"""Constructor for OutputEngine class."""
Expand All @@ -700,7 +703,6 @@ def __init__(
self.affected_versions = affected_versions
self.all_cve_data = all_cve_data
self.detailed = detailed
self.vex_filename = vex_filename
self.exploits = exploits
self.metrics = metrics
self.all_product_data = all_product_data
Expand All @@ -710,6 +712,9 @@ def __init__(
self.sbom_root = sbom_root
self.offline = offline
self.sbom_packages = {}
self.vex_type = vex_type
self.vex_product_info = vex_product_info
self.vex_filename = vex_filename

def output_cves(self, outfile, output_type="console"):
"""Output a list of CVEs
Expand Down Expand Up @@ -788,7 +793,16 @@ def output_cves(self, outfile, output_type="console"):
self.logger.info(f"Output stored at {self.append}")

if self.vex_filename != "":
self.generate_vex(self.all_cve_data, self.vex_filename)
vexgen = VEXGenerate(
self.vex_product_info["product"],
self.vex_product_info["release"],
self.vex_product_info["vendor"],
self.vex_filename,
self.vex_type,
self.all_cve_data,
logger=self.logger,
)
vexgen.generate_vex()
if self.sbom_filename != "":
sbomgen = SBOMGenerate(
self.all_product_data,
Expand All @@ -800,110 +814,6 @@ def output_cves(self, outfile, output_type="console"):
)
sbomgen.generate_sbom()

def generate_vex(self, all_cve_data: dict[ProductInfo, CVEData], filename: str):
"""Generate a vex file and create vulnerability entry."""
analysis_state = {
Remarks.NewFound: "in_triage",
Remarks.Unexplored: "in_triage",
Remarks.Confirmed: "exploitable",
Remarks.Mitigated: "resolved",
Remarks.FalsePositive: "false_positive",
Remarks.NotAffected: "not_affected",
}
response_state = {
Remarks.NewFound: [],
Remarks.Unexplored: [],
Remarks.Confirmed: ["update"],
Remarks.Mitigated: [],
Remarks.FalsePositive: [],
Remarks.NotAffected: [],
}
# URLs for vulnerability detail
source_url = {
"GAD": "https://nvd.nist.gov/vuln/detail/",
"NVD": "https://nvd.nist.gov/vuln/detail/",
"OSV": "https://osv.dev/list?ecosystem=&q=",
"RSD": "https://nvd.nist.gov/vuln/detail/",
"REDHAT": "https://access.redhat.com/security/cve/",
}
# Generate VEX file
vex_output = {"bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1}
# Extra info considered useful
# "creationInfo": {
# "created": datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ"),
# "creators": ["Tool: cve_bin_tool", "Version:" + VERSION],
# },
# "documentDescribes": ["VEX_File"],
# "externalDocumentRefs": [{
# "sbomDocument": "<FILENAME>"
# }],
# }
vuln_entry = []
for product_info, cve_data in all_cve_data.items():
for cve in cve_data["cves"]:
if isinstance(cve, str):
continue
# Create vulnerability entry. Contains id, scoring, analysis and affected component
vulnerability: dict[str, Any] = dict()
id = cve.cve_number
vulnerability["id"] = id
vulnerability["source"] = {
"name": cve.data_source,
"url": source_url[cve.data_source] + id,
}
# Assume CVSS vulnerability scores are in accordance with NVD guidance
if cve.cvss_version == 3:
url = f"v3-calculator?name={cve.cve_number}&vector={cve.cvss_vector}&version=3.1"
else:
url = f"v2-calculator?name={cve.cve_number}&vector={cve.cvss_vector}&version=2.0"
ratings = [
{
"source": {
"name": "NVD",
"url": "https://nvd.nist.gov/vuln-metrics/cvss/" + url,
},
"score": cve.score,
"severity": cve.severity.lower(),
"method": "CVSSv" + str(cve.cvss_version),
"vector": cve.cvss_vector,
}
]
vulnerability["ratings"] = ratings
vulnerability["description"] = cve.description
vulnerability["recommendation"] = ""
vulnerability["advisories"] = []
vulnerability["created"] = "NOT_KNOWN"
vulnerability["published"] = "NOT_KNOWN"
vulnerability["updated"] = cve.last_modified
detail = (
cve.remarks.name + ": " + cve.comments
if cve.comments
else cve.remarks.name
)

analysis = {
"state": analysis_state[cve.remarks],
"response": cve.response or response_state[cve.remarks],
"detail": detail,
}
if cve.justification:
analysis["justification"] = cve.justification
vulnerability["analysis"] = analysis
bom_version = 1
# cve-bin-tool specific reference string to include vendor information
vulnerability["affects"] = [
{
"ref": f"urn:cbt:{bom_version}/{product_info.vendor}#{product_info.product}:{product_info.version}",
}
]
vuln_entry.append(vulnerability)

vex_output["vulnerabilities"] = vuln_entry

# Generate file
with open(filename, "w") as outfile:
json.dump(vex_output, outfile, indent=" ")

def output_file_wrapper(self, output_types=["console"]):
"""Call output_file method for all output types."""
for output_type in output_types:
Expand Down
3 changes: 2 additions & 1 deletion cve_bin_tool/vex_manager/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ def get_vulnerabilities(self) -> List[Vulnerability]:
vulnerability.set_status(self.analysis_state[self.vextype][cve.remarks])
if cve.justification:
vulnerability.set_justification(cve.justification)
# vulnerability.set_remediation(cve.response)
if cve.response:
vulnerability.set_value("remediation", cve.response[0])
detail = (
f"{cve.remarks.name}: {cve.comments}"
if cve.comments
Expand Down
2 changes: 2 additions & 0 deletions test/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -755,6 +755,7 @@ def test_console_output_depending_reportlab_existence(self, caplog):
"log_level : info",
"nvd_api_key : ",
"offline : false",
"vex_file : ",
],
]
tomls = [
Expand Down Expand Up @@ -783,6 +784,7 @@ def test_console_output_depending_reportlab_existence(self, caplog):
"extract = true",
"append = false",
'import = ""',
'vex_file = ""',
],
]

Expand Down
Loading