Skip to content

Commit 436a203

Browse files
authored
feat: improved triage process (#4279)
Signed-off-by: [email protected] <[email protected]>
1 parent aadf42d commit 436a203

File tree

12 files changed

+457
-425
lines changed

12 files changed

+457
-425
lines changed

cve_bin_tool/cli.py

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@
7575
from cve_bin_tool.util import ProductInfo
7676
from cve_bin_tool.version import VERSION
7777
from cve_bin_tool.version_scanner import VersionScanner
78+
from cve_bin_tool.vex_manager.parse import VEXParse
7879

7980
sys.excepthook = excepthook # Always install excepthook for entrypoint module.
8081

@@ -180,12 +181,6 @@ def main(argv=None):
180181
default="",
181182
help="provide input filename",
182183
)
183-
input_group.add_argument(
184-
"--triage-input-file",
185-
action="store",
186-
default="",
187-
help="provide input filename for triage data",
188-
)
189184
input_group.add_argument(
190185
"-C", "--config", action="store", default="", help="provide config file"
191186
)
@@ -358,7 +353,7 @@ def main(argv=None):
358353
output_group.add_argument(
359354
"--vex-type",
360355
action="store",
361-
default="cyclonedx",
356+
default="",
362357
choices=["cyclonedx", "csaf", "openvex"],
363358
help="specify type of vulnerability exchange (vex) to generate (default: cyclonedx)",
364359
)
@@ -380,6 +375,19 @@ def main(argv=None):
380375
default="",
381376
help="Vendor/Supplier of Product",
382377
)
378+
output_group.add_argument(
379+
"-rr",
380+
"--revision-reason",
381+
action="store",
382+
default="",
383+
help="a reason for the update to the vex document should be specified in double quotes",
384+
)
385+
output_group.add_argument(
386+
"--filter-triage",
387+
action="store_true",
388+
default=False,
389+
help="Filter cves based on triage data from Vex file",
390+
)
383391
parser.add_argument(
384392
"-e",
385393
"--exclude",
@@ -917,6 +925,7 @@ def main(argv=None):
917925
and not args["package_list"]
918926
and not args["merge"]
919927
and not args["sbom_file"]
928+
and not args["vex_file"]
920929
):
921930
parser.print_usage()
922931
with ErrorHandler(logger=LOGGER, mode=ErrorMode.NoTrace):
@@ -1009,7 +1018,7 @@ def main(argv=None):
10091018
triage_data: TriageData
10101019
total_files: int = 0
10111020
parsed_data: dict[ProductInfo, TriageData] = {}
1012-
1021+
vex_product_info: dict[str, str] = {}
10131022
# Package List parsing
10141023
if args["package_list"]:
10151024
sbom_root = args["package_list"]
@@ -1021,18 +1030,6 @@ def main(argv=None):
10211030
LOGGER.debug(f"{product_info}, {triage_data}")
10221031
cve_scanner.get_cves(product_info, triage_data)
10231032

1024-
if args["triage_input_file"]:
1025-
input_engine = InputEngine(
1026-
args["triage_input_file"],
1027-
logger=LOGGER,
1028-
error_mode=error_mode,
1029-
filetype="vex",
1030-
)
1031-
parsed_data = input_engine.parse_input()
1032-
for product_info, triage_data in parsed_data.items():
1033-
LOGGER.debug(f"{product_info}, {triage_data}")
1034-
cve_scanner.get_cves(product_info, triage_data)
1035-
10361033
if args["input_file"]:
10371034
input_engine = InputEngine(
10381035
args["input_file"], logger=LOGGER, error_mode=error_mode
@@ -1092,6 +1089,41 @@ def main(argv=None):
10921089
LOGGER.debug(f"{product_info}, {triage_data}")
10931090
cve_scanner.get_cves(product_info, triage_data)
10941091

1092+
if args["vex_file"]:
1093+
# for now use cyclonedx as auto detection is not implemented in latest pypi package of lib4vex
1094+
vexdata = VEXParse(
1095+
filename=args["vex_file"],
1096+
vextype="cyclonedx",
1097+
logger=LOGGER,
1098+
)
1099+
parsed_vex_data = vexdata.parse_vex()
1100+
vex_product_info = vexdata.vex_product_info
1101+
if not parsed_data:
1102+
# assume the vex file being scanned is a standalone file
1103+
args["filter_triage"] = False
1104+
parsed_data = parsed_vex_data
1105+
for product_info, triage_data in parsed_data.items():
1106+
LOGGER.debug(f"{product_info}, {triage_data}")
1107+
cve_scanner.get_cves(product_info, triage_data)
1108+
else:
1109+
LOGGER.info(
1110+
f"VEX file {args['vex_file']} is not a standalone file and will be used as a triage file"
1111+
)
1112+
# need to do validation on the sbom part
1113+
# need to implement is_linked() function which will check the linkage.
1114+
if args["sbom_file"]:
1115+
LOGGER.warning(
1116+
f"SBOM file: {args['sbom_file']} is not linked to VEX file: {args['vex_file']}."
1117+
)
1118+
for product_info, triage_data in parsed_vex_data.items():
1119+
LOGGER.debug(f"{product_info}, {triage_data}")
1120+
if product_info in parsed_data:
1121+
cve_scanner.get_cves(product_info, triage_data)
1122+
else:
1123+
LOGGER.info(
1124+
f"Product: {product_info.product} with Version: {product_info.version} not found in Parsed Data, is valid vex file being used?"
1125+
)
1126+
10951127
LOGGER.info("Overall CVE summary: ")
10961128
LOGGER.info(
10971129
f"There are {cve_scanner.products_with_cve} products with known CVEs detected"
@@ -1105,19 +1137,29 @@ def main(argv=None):
11051137
)
11061138
)
11071139
LOGGER.info(f"Known CVEs in {affected_string}:")
1108-
vex_product_info: dict[str, str] = {}
1109-
if args["vex_output"]:
1140+
if args["vex_type"] or args["vex_output"]:
1141+
# If vex_type is provided, then use it, else use cyclonedx as default vex_output should be provide in this case
1142+
# If vex_output is provided, then use it, else use product, release and vendor to generate the vex file.
1143+
if args["vex_output"] and not args["vex_type"]:
1144+
# default vex_type is cyclonedx
1145+
args["vex_type"] = "cyclonedx"
11101146
if args["product"] and args["release"] and args["vendor"]:
11111147
vex_product_info = {
11121148
"product": args["product"],
11131149
"release": args["release"],
11141150
"vendor": args["vendor"],
1151+
"revision_reason": args["revision_reason"],
11151152
}
1153+
elif args["vex_file"]:
1154+
vex_product_info["revision_reason"] = args["revision_reason"]
11161155
else:
11171156
LOGGER.error(
11181157
"Please provide --product, --release and --vendor for VEX generation"
11191158
)
11201159
return ERROR_CODES[InsufficientArgs]
1160+
1161+
if args["vex_file"] and args["filter_triage"]:
1162+
cve_scanner.filter_triage_data()
11211163
# Creates an Object for OutputEngine
11221164
output = OutputEngine(
11231165
all_cve_data=cve_scanner.all_cve_data,

cve_bin_tool/cve_scanner.py

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,12 +82,42 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
8282
return
8383

8484
if product_info in self.all_cve_data:
85-
# If product_info already in all_cve_data no need to fetch cves from database again
86-
# We just need to update paths.
85+
# If product_info already in all_cve_data, no need to fetch CVEs from the database again.
86+
# We just need to update paths and triage data.
8787
self.logger.debug(
88-
f"{product_info} already processed. Update path {triage_data['paths']}"
88+
f"{product_info} already processed. Update paths {triage_data['paths']}"
8989
)
90-
# self.products_with_cve += 1
90+
91+
# Update the triage data
92+
cve_data = self.all_cve_data[product_info]["cves"]
93+
new_cve_data = []
94+
95+
for cve in cve_data:
96+
cve_number = cve.cve_number
97+
if cve_number in triage_data:
98+
for key in [
99+
"remarks",
100+
"comments",
101+
"response",
102+
"justification",
103+
"severity",
104+
]:
105+
data = triage_data[cve_number].get(key)
106+
if data:
107+
if (
108+
key == "severity"
109+
and self.check_exploits
110+
and cve_number in self.exploits_list
111+
):
112+
data += "-EXPLOIT"
113+
114+
self.logger.debug(f"Setting field {key} to: {data}")
115+
cve = cve._replace(**{key: data})
116+
new_cve_data.append(cve)
117+
118+
self.all_cve_data[product_info]["cves"] = new_cve_data
119+
120+
# Update paths
91121
self.all_cve_data[product_info]["paths"] |= set(triage_data["paths"])
92122
return
93123

@@ -350,6 +380,43 @@ def get_cves(self, product_info: ProductInfo, triage_data: TriageData):
350380
if product_info not in self.all_product_data:
351381
self.all_product_data[product_info] = len(cves)
352382

383+
def filter_triage_data(self):
384+
"""
385+
Filter out triage data that is not relevant to the CVEs found,
386+
specifically those marked as NotAffected or FalsePositives.
387+
"""
388+
to_delete: List[ProductInfo] = []
389+
390+
for product_info, cve_data in self.all_cve_data.items():
391+
original_cves = cve_data["cves"]
392+
filtered_cves = []
393+
filtered_out_cves = []
394+
395+
for cve in original_cves:
396+
if cve.remarks not in {Remarks.NotAffected, Remarks.FalsePositive}:
397+
filtered_cves.append(cve)
398+
else:
399+
filtered_out_cves.append(cve)
400+
401+
for cve in filtered_out_cves:
402+
self.logger.info(
403+
f"Filtered CVE: {cve.cve_number} for Product: {product_info.product}"
404+
)
405+
406+
if filtered_cves:
407+
cve_data["cves"] = filtered_cves
408+
self.logger.debug(
409+
f"Filtered triage data for {product_info.product}: {[cve.cve_number for cve in filtered_cves]}"
410+
)
411+
else:
412+
to_delete.append(product_info)
413+
414+
for product_info in to_delete:
415+
del self.all_cve_data[product_info]
416+
self.logger.debug(
417+
f"Removed product info for {product_info.product} due to no relevant CVEs"
418+
)
419+
353420
def affected(self):
354421
"""Returns list of vendor.product and version tuples identified from
355422
scan"""

0 commit comments

Comments
 (0)