Skip to content

Commit 7f327c9

Browse files
feat: add support for VEX (Fixes #1570) (#1583)
1 parent 84715ba commit 7f327c9

File tree

9 files changed

+618
-2
lines changed

9 files changed

+618
-2
lines changed

README.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@ cve-bin-tool --input-file <filename>
7575
```
7676

7777
Note that the `--input-file` option can also be used to add extra triage data like remarks, comments etc. while scanning a directory so that output will reflect this triage data and you can save time of re-triaging (Usage: `cve-bin-tool -i=test.csv /path/to/scan`).
78+
A VEX file (which may be created using the `--vex` command line option) can also be used as a triage file. A VEX file
79+
is detected if the file suffix is '.vex'.
7880

7981
### Scanning an SBOM file for known vulnerabilities
8082

@@ -91,6 +93,11 @@ Valid SBOM types are [SPDX](https://spdx.dev/specifications/),
9193

9294
The CVE Binary Tool provides console-based output by default. If you wish to provide another format, you can specify this and a filename on the command line using `--format`. The valid formats are CSV, JSON, console, HTML and PDF. The output filename can be specified using the `--output-file` flag.
9395

96+
The reported vulnerabilities can additionally be reported in the
97+
Vulnerability Exchange (VEX) format by specifying `--vex` command line option.
98+
The generated VEX file can then be used as an `--input-file` to support
99+
a triage process.
100+
94101
## Full option list
95102

96103
Usage:
@@ -142,9 +149,12 @@ Usage:
142149
minimum CVE severity to report (default: low)
143150
--report Produces a report even if there are no CVE for the
144151
respective output format
145-
--affected-versions Lists versions of product affected by a given CVE (to facilitate upgrades)
152+
-A [<distro_name>-<distro_version_name>], --available-fix [<distro_name>-<distro_version_name>]
153+
Lists available fixes of the package from Linux distribution
146154
-b [<distro_name>-<distro_version_name>], --backport-fix [<distro_name>-<distro_version_name>]
147155
Lists backported fixes if available from Linux distribution
156+
--affected-versions Lists versions of product affected by a given CVE (to facilitate upgrades)
157+
--vex VEX Provide vulnerability exchange (vex) filename
148158

149159
Merge Report:
150160
-a INTERMEDIATE_PATH, --append INTERMEDIATE_PATH

cve_bin_tool/cli.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,13 @@ def main(argv=None):
240240
help="Lists versions of product affected by a given CVE (to facilitate upgrades)",
241241
)
242242

243+
output_group.add_argument(
244+
"--vex",
245+
action="store",
246+
help="Provide vulnerability exchange (vex) filename",
247+
default="",
248+
)
249+
243250
parser.add_argument(
244251
"-e",
245252
"--exclude",
@@ -614,6 +621,7 @@ def main(argv=None):
614621
append=args["append"],
615622
merge_report=merged_reports,
616623
affected_versions=args["affected_versions"],
624+
vex_filename=args["vex"],
617625
)
618626

619627
if not args["quiet"]:

cve_bin_tool/input_engine.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,12 @@
44
import csv
55
import json
66
import os
7+
import re
78
from collections import defaultdict
89
from logging import Logger
910
from typing import Any, DefaultDict, Dict, Iterable, Set, Union
1011

12+
from cve_bin_tool.cvedb import CVEDB
1113
from cve_bin_tool.error_handler import (
1214
ErrorHandler,
1315
ErrorMode,
@@ -33,6 +35,8 @@ def __init__(
3335
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
3436
self.error_mode = error_mode
3537
self.parsed_data = defaultdict(dict)
38+
# Connect to the database
39+
self.cvedb = CVEDB(version_check=False)
3640

3741
def parse_input(self) -> DefaultDict[ProductInfo, TriageData]:
3842
if not os.path.isfile(self.filename):
@@ -42,6 +46,8 @@ def parse_input(self) -> DefaultDict[ProductInfo, TriageData]:
4246
self.input_csv()
4347
elif self.filename.endswith(".json"):
4448
self.input_json()
49+
elif self.filename.endswith(".vex"):
50+
self.input_vex()
4551
return self.parsed_data
4652

4753
def input_csv(self) -> None:
@@ -62,6 +68,62 @@ def input_json(self) -> None:
6268

6369
self.parse_data(set(json_data[0].keys()), json_data)
6470

71+
def validate_product(self, product: str) -> bool:
72+
"""
73+
Ensure product name conforms to CPE 2.3 standard.
74+
See https://csrc.nist.gov/schema/cpe/2.3/cpe-naming_2.3.xsd for naming specification
75+
"""
76+
cpe_regex = r"\A([A-Za-z0-9\._\-~ %])+\Z"
77+
return re.search(cpe_regex, product) is not None
78+
79+
def input_vex(self) -> None:
80+
analysis_state = {
81+
"under_review": Remarks.Unexplored,
82+
"in_triage": Remarks.Unexplored,
83+
"exploitable": Remarks.Confirmed,
84+
"not_affected": Remarks.Mitigated,
85+
}
86+
with open(self.filename) as json_file:
87+
json_data = json.load(json_file)
88+
# Only handle CycloneDX VEX file format
89+
if json_data["bomFormat"] == "CycloneDX":
90+
for vulnerability in json_data["vulnerabilities"]:
91+
id = vulnerability["id"]
92+
analysis_status = vulnerability["analysis"]["state"].lower()
93+
state = Remarks.Unexplored
94+
if analysis_status in analysis_state:
95+
state = analysis_state[analysis_status]
96+
comments = vulnerability["analysis"]["detail"]
97+
for rating in vulnerability["ratings"]:
98+
severity = rating["severity"]
99+
for affect in vulnerability["affects"]:
100+
ref = affect["ref"]
101+
version = None
102+
vendor = None
103+
if "#" in ref:
104+
# Extract product information after # delimiter
105+
p = ref.split("#")[1]
106+
# Last element is version, rest is product which may include -
107+
elements = p.rsplit("-", 1)
108+
product = elements[0]
109+
version = elements[1]
110+
# Now find vendor
111+
vendor_package_pair = self.cvedb.get_vendor_product_pairs(
112+
product
113+
)
114+
if vendor_package_pair != []:
115+
vendor = vendor_package_pair[0]["vendor"]
116+
if version is not None and self.validate_product(product):
117+
product_info = ProductInfo(
118+
vendor.strip(), product.strip(), version.strip()
119+
)
120+
self.parsed_data[product_info][id.strip() or "default"] = {
121+
"remarks": state,
122+
"comments": comments.strip(),
123+
"severity": severity.strip(),
124+
}
125+
self.parsed_data[product_info]["paths"] = {""}
126+
65127
def parse_data(self, fields: Set[str], data: Iterable) -> None:
66128
required_fields = {"vendor", "product", "version"}
67129
missing_fields = required_fields - fields

cve_bin_tool/output_engine/__init__.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from ..cvedb import CVEDB
1414
from ..error_handler import ErrorHandler, ErrorMode
1515
from ..log import LOGGER
16-
from ..util import ProductInfo
16+
from ..util import ProductInfo, Remarks
1717
from ..version import VERSION
1818
from . import pdfbuilder
1919
from .console import output_console
@@ -305,6 +305,7 @@ def __init__(
305305
merge_report: Union[None, List[str]] = None,
306306
affected_versions: int = 0,
307307
all_cve_version_info=None,
308+
vex_filename: str = "",
308309
):
309310
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
310311
self.all_cve_version_info = all_cve_version_info
@@ -321,6 +322,7 @@ def __init__(
321322
self.merge_report = merge_report
322323
self.affected_versions = affected_versions
323324
self.all_cve_data = all_cve_data
325+
self.vex_filename = vex_filename
324326

325327
def output_cves(self, outfile, output_type="console"):
326328
"""Output a list of CVEs
@@ -372,6 +374,92 @@ def output_cves(self, outfile, output_type="console"):
372374
)
373375
self.logger.info(f"Output stored at {self.append}")
374376

377+
if self.vex_filename != "":
378+
self.generate_vex(self.all_cve_data, self.vex_filename)
379+
380+
def generate_vex(self, all_cve_data: Dict[ProductInfo, CVEData], filename: str):
381+
analysis_state = {
382+
Remarks.NewFound: "under_review",
383+
Remarks.Unexplored: "under_review",
384+
Remarks.Confirmed: "exploitable",
385+
Remarks.Mitigated: "not_affected",
386+
Remarks.Ignored: "not_affected",
387+
}
388+
response_state = {
389+
Remarks.NewFound: "Outstanding",
390+
Remarks.Unexplored: "Not defined",
391+
Remarks.Confirmed: "Upgrade required",
392+
Remarks.Mitigated: "Resolved",
393+
Remarks.Ignored: "No impact",
394+
}
395+
# Generate VEX file
396+
vex_output = {"bomFormat": "CycloneDX", "specVersion": "1.4", "version": 1}
397+
# Extra info considered useful
398+
# "creationInfo": {
399+
# "created": datetime.now().strftime("%Y-%m-%dT%H-%M-%SZ"),
400+
# "creators": ["Tool: cve_bin_tool", "Version:" + VERSION],
401+
# },
402+
# "documentDescribes": ["VEX_File"],
403+
# "externalDocumentRefs": [{
404+
# "sbomDocument": "<FILENAME>"
405+
# }],
406+
# }
407+
vuln_entry = []
408+
for product_info, cve_data in all_cve_data.items():
409+
for cve in cve_data["cves"]:
410+
# Create vulnerability entry. Contains id, scoring, analysis and affected component
411+
vulnerability = dict()
412+
vulnerability["id"] = cve.cve_number
413+
vulnerability["source"] = {
414+
"name": "NVD",
415+
"url": "https://nvd.nist.gov/vuln/detail/" + cve.cve_number,
416+
}
417+
if cve.cvss_version == 3:
418+
url = f"v3-calculator?name={cve.cve_number}&vector={cve.cvss_vector}&version=3.1"
419+
else:
420+
url = f"v2-calculator?name={cve.cve_number}&vector={cve.cvss_vector}&version=2.0"
421+
ratings = [
422+
{
423+
"source": {
424+
"name": "NVD",
425+
"url": "https://nvd.nist.gov/vuln-metrics/cvss/" + url,
426+
},
427+
"score": str(cve.score),
428+
"severity": cve.severity,
429+
"method": "CVSSv" + str(cve.cvss_version),
430+
"vector": cve.cvss_vector,
431+
}
432+
]
433+
vulnerability["ratings"] = ratings
434+
vulnerability["cwes"] = []
435+
vulnerability["description"] = cve.description
436+
vulnerability["recommendation"] = ""
437+
vulnerability["advisories"] = []
438+
vulnerability["created"] = "NOT_KNOWN"
439+
vulnerability["published"] = "NOT_KNOWN"
440+
vulnerability["updated"] = "NOT_KNOWN"
441+
analysis = {
442+
"state": analysis_state[cve.remarks],
443+
"response": response_state[cve.remarks],
444+
"justification": "",
445+
"detail": cve.comments,
446+
}
447+
vulnerability["analysis"] = analysis
448+
bom_urn = "NOTKNOWN"
449+
bom_version = 1
450+
vulnerability["affects"] = [
451+
{
452+
"ref": f"urn:cdx:{bom_urn}/{bom_version}#{product_info.product}-{product_info.version}",
453+
}
454+
]
455+
vuln_entry.append(vulnerability)
456+
457+
vex_output["vulnerabilities"] = vuln_entry
458+
459+
# Generate file
460+
with open(filename, "w") as outfile:
461+
json.dump(vex_output, outfile, indent=" ")
462+
375463
def output_file(self, output_type="console"):
376464

377465
"""Generate a file for list of CVE"""

doc/MANUAL.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@
3131
- [-c CVSS, --cvss CVSS](#-c-cvss---cvss-cvss)
3232
- [-S {low,medium,high,critical}, --severity {low,medium,high,critical}](#-s-lowmediumhighcritical---severity-lowmediumhighcritical)
3333
- [--report](#--report)
34+
- [-A [<distro_name>-<distro_version_name>], --available-fix [<distro_name>-<distro_version_name>]](#-A-distro_name-distro_version_name---available-fix-distro_name-distro_version_name)
3435
- [-b [<distro_name>-<distro_version_name>], --backport-fix [<distro_name>-<distro_version_name>]](#-b-distro_name-distro_version_name---backport-fix-distro_name-distro_version_name)
36+
- [--affected-versions](#--affected-versions)
37+
- [--vex VEX_FILE](#--vex-vex_file)
3538
- [Output verbosity](#output-verbosity)
3639
- [Quiet Mode](#quiet-mode)
3740
- [Logging modes](#logging-modes)
@@ -97,8 +100,12 @@ which is useful if you're trying the latest code from
97100
minimum CVE severity to report (default: low)
98101
--report Produces a report even if there are no CVE for the
99102
respective output format
103+
-A [<distro_name>-<distro_version_name>], --available-fix [<distro_name>-<distro_version_name>]
104+
Lists available fixes of the package from Linux distribution
100105
-b [<distro_name>-<distro_version_name>], --backport-fix [<distro_name>-<distro_version_name>]
101106
Lists backported fixes if available from Linux distribution
107+
--affected-versions Lists versions of product affected by a given CVE (to facilitate upgrades)
108+
--vex VEX Provide vulnerability exchange (vex) filename
102109

103110
Merge Report:
104111
-a INTERMEDIATE_PATH, --append INTERMEDIATE_PATH
@@ -309,6 +316,19 @@ You can provide either CSV or JSON file as input_file with vendor, product and v
309316
4. **severity** - This field allows you to adjust severity score of specific product or CVE. This can be useful in the case where CVE affects a portion of the library that you aren't using currently but you don't want to ignore it completely. In that case, you can reduce severity for this CVE.
310317
5. **cve_number** - This field give you fine grained control over output of specific CVE. You can change remarks, comments and severity for specific CVE instead of whole product.
311318

319+
You can also provide a Vulnerability Exchange (VEX) file which contains the reported vulnerabilities for components within a product. The supported format
320+
is the [CycloneDX](https://cyclonedx.org/capabilities/vex/) VEX format which can be generated using the `--vex` option. A VEX file is identified with a file extension of .vex.
321+
For the triage process, the **state** value in the analysis section of each CVE should have one of the following values:
322+
323+
```
324+
"under_review" - this is the default state and should be used to indicate the vulnerability is to be reviewed
325+
"in_triage" - this should be used to indicate that the vulnerability is being reviewed
326+
"exploitable" - this should be used to indicate that the vulnerability is known to be exploitable
327+
"not_affected" - this should be used to indicate that the vulnerability has been mitigated
328+
```
329+
330+
The **detail** value in the analysis section can be used to provide comments related to the state
331+
312332
You can use `-i` or `--input-file` option to produce list of CVEs found in given vendor, product and version fields (Usage: `cve-bin-tool -i=test.csv`) or supplement extra triage data like remarks, comments etc. while scanning directory so that output will reflect this triage data and you can save time of re-triaging (Usage: `cve-bin-tool -i=test.csv /path/to/scan`).
313333

314334
Note that `--input-file`, unlike `cve-bin-tool directory` scan, will work on *any* product known in the National Vulnerability Database, not only those that have checkers written.
@@ -615,6 +635,30 @@ Note that this option is overridden by `--cvss` parameter if this is also specif
615635

616636
This option produces a report for all output formats even if there are 0 CVEs. By default CVE Binary tool doesn't produce an output when there are 0 CVEs.
617637

638+
### -A [<distro_name>-<distro_version_name>], --available-fix [<distro_name>-<distro_version_name>]
639+
640+
This option lists the available fixes of the package from Linux distribution if there are any.
641+
642+
The currently supported Linux distributions are:
643+
644+
```
645+
debian-bullseye
646+
debian-stretch
647+
debian-buster
648+
ubuntu-hirsute
649+
ubuntu-groovy
650+
ubuntu-focal
651+
ubuntu-eoan
652+
ubuntu-disco
653+
ubuntu-cosmic
654+
ubuntu-bionic
655+
ubuntu-artful
656+
ubuntu-zesty
657+
ubuntu-yakkety
658+
ubuntu-xenial
659+
```
660+
661+
618662
### -b [<distro_name>-<distro_version_name>], --backport-fix [<distro_name>-<distro_version_name>]
619663

620664
This option outputs the available backported fixes for the packages with CVEs if there are any.
@@ -644,6 +688,17 @@ ubuntu-yakkety
644688
ubuntu-xenial
645689
```
646690

691+
### --affected-versions
692+
693+
This options reports the versions of a product affected by a given CVE.
694+
695+
### --vex VEX_FILE
696+
697+
This option allows you to specify the filename for a Vulnerability Exchange (VEX)
698+
file which contains all the reported vulnerabilities detected by the scan. This file is typically
699+
updated (outside of the CVE Binary tool) to record the results of a triage activity
700+
and can be used as a file with `--input-file` parameter.
701+
647702
### Output verbosity
648703

649704
As well as the modes above, there are two other output options to decrease or increase the number of messages printed:

0 commit comments

Comments
 (0)