Skip to content

Commit c546b8c

Browse files
authored
feat: Update MergeReport based HTML Report triage (#1204)
* feat: Update MergeReport and add split intermediate graph in html reports * feat: Add severity count traves in intermediate html reports * Update docstring quotes in html.py * update docstring and add new line in html
1 parent dbf2a6b commit c546b8c

File tree

10 files changed

+443
-70
lines changed

10 files changed

+443
-70
lines changed

cve_bin_tool/cli.py

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -262,17 +262,28 @@ def main(argv=None):
262262
**********************************************
263263
"""
264264
LOGGER.warning(warning_nolinux)
265+
266+
# CSVScanner related settings
267+
score = 0
268+
if args["severity"]:
269+
# Set minimum CVSS score based on severity
270+
cvss_score = {"low": 0, "medium": 4, "high": 7, "critical": 9}
271+
score = cvss_score[args["severity"]]
272+
if int(args["cvss"]) > 0:
273+
score = int(args["cvss"])
274+
275+
merged_reports = None
265276
if args["merge"]:
266277
LOGGER.info(
267278
"You can use -f --format and -o --output-file for saving merged intermediate reports in a file"
268279
)
269-
merged_cves = MergeReports(merge_files=args["merge"])
280+
merged_reports = MergeReports(merge_files=args["merge"], score=score)
270281
if args["input_file"]:
271282
LOGGER.warning(
272283
"Ignoring -i --input-file while merging intermediate reports"
273284
)
274-
args["input_file"] = merged_cves.merge_reports()
275-
# Creates a Object for OutputEngine
285+
args["input_file"] = None
286+
merge_cve_scanner = merged_reports.merge_intermediate()
276287

277288
# Database update related settings
278289
# Connect to the database
@@ -311,7 +322,12 @@ def main(argv=None):
311322
cvedb_orig.remove_cache_backup()
312323

313324
# Input validation
314-
if not args["directory"] and not args["input_file"] and not args["package_list"]:
325+
if (
326+
not args["directory"]
327+
and not args["input_file"]
328+
and not args["package_list"]
329+
and not args["merge"]
330+
):
315331
parser.print_usage()
316332
with ErrorHandler(logger=LOGGER, mode=ErrorMode.NoTrace):
317333
raise InsufficientArgs(
@@ -343,15 +359,6 @@ def main(argv=None):
343359
)
344360
)
345361

346-
# CSVScanner related settings
347-
score = 0
348-
if args["severity"]:
349-
# Set minimum CVSS score based on severity
350-
cvss_score = {"low": 0, "medium": 4, "high": 7, "critical": 9}
351-
score = cvss_score[args["severity"]]
352-
if int(args["cvss"]) > 0:
353-
score = int(args["cvss"])
354-
355362
with CVEScanner(score=score) as cve_scanner:
356363
triage_data: TriageData
357364
total_files: int = 0
@@ -394,6 +401,9 @@ def main(argv=None):
394401
cve_scanner.get_cves(product_info, triage_data)
395402
total_files = version_scanner.total_scanned_files
396403

404+
if args["merge"]:
405+
cve_scanner = merge_cve_scanner
406+
397407
LOGGER.info("")
398408
LOGGER.info("Overall CVE summary: ")
399409
if args["input_file"] or args["package_list"]:
@@ -426,10 +436,8 @@ def main(argv=None):
426436
total_files=total_files,
427437
is_report=args["report"],
428438
append=args["append"],
439+
merge_report=merged_reports,
429440
)
430-
if args["merge"] and args["input_file"]:
431-
# remove the merged json from .cache
432-
os.remove(args["input_file"])
433441

434442
if not args["quiet"]:
435443
output.output_file(args["format"])

cve_bin_tool/merge.py

Lines changed: 93 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33

44
import json
55
import os
6+
from collections import defaultdict
67
from datetime import datetime
78
from logging import Logger
89
from typing import Dict, List
910

11+
from cve_bin_tool.cve_scanner import CVEScanner
12+
1013
from .cvedb import DISK_LOCATION_DEFAULT
1114
from .error_handler import (
1215
ErrorHandler,
@@ -15,8 +18,9 @@
1518
InvalidJsonError,
1619
MissingFieldsError,
1720
)
21+
from .input_engine import TriageData
1822
from .log import LOGGER
19-
from .util import DirWalk
23+
from .util import DirWalk, ProductInfo, Remarks
2024

2125
REQUIRED_INTERMEDIATE_METADATA = {
2226
"scanned_dir",
@@ -35,9 +39,11 @@ def __init__(
3539
logger: Logger = None,
3640
error_mode=ErrorMode.TruncTrace,
3741
cache_dir=DISK_LOCATION_DEFAULT,
42+
score=0,
3843
):
3944
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
4045
self.merge_files = merge_files
46+
self.intermediate_cve_data = []
4147
self.all_cve_data = []
4248
self.file_stack = []
4349
self.error_mode = error_mode
@@ -46,6 +52,8 @@ def __init__(
4652
self.products_with_cve = 0
4753
self.products_without_cve = 0
4854
self.cache_dir = cache_dir
55+
self.merged_files = ["tag"]
56+
self.score = score
4957

5058
self.walker = DirWalk(
5159
pattern=";".join(
@@ -97,6 +105,9 @@ def scan_intermediate_file(self, filename):
97105
f"Adding data from {os.path.basename(filename)} with timestamp {inter_data['metadata']['timestamp']}"
98106
)
99107
self.total_inter_files += 1
108+
inter_data["metadata"]["severity"] = get_severity_count(
109+
inter_data["report"]
110+
)
100111
return inter_data
101112

102113
if missing_fields != set():
@@ -106,41 +117,41 @@ def scan_intermediate_file(self, filename):
106117
with ErrorHandler(mode=self.error_mode):
107118
raise InvalidIntermediateJsonError(filename)
108119

109-
def merge_reports(self):
120+
def merge_intermediate(self):
110121
"""Merge valid intermediate dictionaries"""
111122

112123
for inter_file in self.recursive_scan(self.merge_files):
113-
# Remove duplicate paths from cve-entries
114-
self.all_cve_data.append(self.scan_intermediate_file(inter_file))
115-
116-
if self.all_cve_data:
124+
# Create a list of intermediate files dictionary
125+
self.intermediate_cve_data.append(self.scan_intermediate_file(inter_file))
126+
127+
if self.intermediate_cve_data:
128+
# sort on basis of timestamp and scans
129+
self.intermediate_cve_data.sort(
130+
key=lambda inter: datetime.strptime(
131+
inter["metadata"]["timestamp"], "%Y-%m-%d.%H-%M-%S"
132+
)
133+
)
117134
self.all_cve_data = self.remove_intermediate_duplicates()
118-
merged_file_path = self.save_merged_intermediate()
119-
return merged_file_path
120-
121-
self.logger.error("No valid Intermediate reports found!")
122-
return ""
123-
124-
def save_merged_intermediate(self):
125-
"""Save a temporary merged report in .cache/cve-bin-tool"""
135+
merged_cve_scanner = self.get_intermediate_cve_scanner(
136+
[self.all_cve_data], self.score
137+
)[0]
138+
merged_cve_scanner.products_with_cve = self.products_with_cve
139+
merged_cve_scanner.products_without_cve = self.products_without_cve
126140

127-
if not os.path.isdir(self.cache_dir):
128-
os.makedirs(self.cache_dir)
141+
return merged_cve_scanner
129142

130-
now = datetime.now().strftime("%Y-%m-%d.%H-%M-%S")
131-
filename = os.path.join(self.cache_dir, f"merged-{now}.json")
132-
with open(filename, "w") as f:
133-
json.dump(self.all_cve_data, f, indent=" ")
134-
135-
return filename
143+
self.logger.error("No valid Intermediate reports found!")
144+
return {}
136145

137146
def remove_intermediate_duplicates(self) -> List[Dict[str, str]]:
138147
"""Returns a list of dictionary with same format as cve-bin-tool json output"""
139148

140149
output = {}
141-
for inter_data in self.all_cve_data:
142-
self.products_with_cve += inter_data["metadata"]["products_with_cve"]
143-
self.products_without_cve += inter_data["metadata"]["products_without_cve"]
150+
for inter_data in self.intermediate_cve_data:
151+
self.products_with_cve += int(inter_data["metadata"]["products_with_cve"])
152+
self.products_without_cve += int(
153+
inter_data["metadata"]["products_without_cve"]
154+
)
144155
for cve in inter_data["report"]:
145156
if cve["cve_number"] != "UNKNOWN":
146157
if cve["cve_number"] not in output:
@@ -156,3 +167,60 @@ def remove_intermediate_duplicates(self) -> List[Dict[str, str]]:
156167
output[cve["cve_number"]]["path"] = path_list
157168

158169
return list(output.values())
170+
171+
@staticmethod
172+
def get_intermediate_cve_scanner(cve_data_list, score) -> List[CVEScanner]:
173+
"""Returns a list of CVEScanner parsed objects when a list of cve_data json like list is passed"""
174+
cve_scanner_list = []
175+
for inter_data in cve_data_list:
176+
with CVEScanner(score=score) as cve_scanner:
177+
triage_data: TriageData
178+
parsed_data: Dict[ProductInfo, TriageData] = {}
179+
180+
parsed_data = parse_data_from_json(
181+
inter_data["report"] if "report" in inter_data else inter_data
182+
)
183+
184+
for product_info, triage_data in parsed_data.items():
185+
LOGGER.debug(f"{product_info}, {triage_data}")
186+
cve_scanner.get_cves(product_info, triage_data)
187+
188+
cve_scanner_list.append(cve_scanner)
189+
190+
return cve_scanner_list
191+
192+
193+
def parse_data_from_json(
194+
json_data: List[Dict[str, str]]
195+
) -> Dict[ProductInfo, TriageData]:
196+
"""Parse CVE JSON dictionary to Dict[ProductInfo, TriageData]"""
197+
198+
parsed_data: Dict[ProductInfo, TriageData] = defaultdict(dict)
199+
200+
for row in json_data:
201+
product_info = ProductInfo(
202+
row["vendor"].strip(), row["product"].strip(), row["version"].strip()
203+
)
204+
parsed_data[product_info][row.get("cve_number", "").strip() or "default"] = {
205+
"remarks": Remarks(str(row.get("remarks", "")).strip()),
206+
"comments": row.get("comments", "").strip(),
207+
"severity": row.get("severity", "").strip(),
208+
}
209+
210+
parsed_data[product_info]["paths"] = set(
211+
map(lambda x: x.strip(), row.get("paths", "").split(","))
212+
)
213+
return parsed_data
214+
215+
216+
def get_severity_count(reports: List[Dict[str, str]] = []) -> Dict[str, int]:
217+
"""Returns a list of Severity counts for intermediate report"""
218+
severity_count = {"LOW": 0, "MEDIUM": 0, "HIGH": 0, "CRITICAL": 0, "UNKNOWN": 0}
219+
220+
for cve in reports:
221+
if "severity" in cve and cve["severity"] in severity_count:
222+
severity_count[cve["severity"]] += 1
223+
else:
224+
severity_count["UNKNOWN"] += 1
225+
226+
return severity_count

cve_bin_tool/output_engine/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import os
77
import time
88
from logging import Logger
9-
from typing import IO, Dict, Union
9+
from typing import IO, Dict, List, Union
1010

1111
from ..cve_scanner import CVEData
1212
from ..cvedb import CVEDB
@@ -200,6 +200,7 @@ def __init__(
200200
total_files: int = 0,
201201
is_report: bool = False,
202202
append: Union[str, bool] = False,
203+
merge_report: Union[None, List[str]] = None,
203204
):
204205
self.logger = logger or LOGGER.getChild(self.__class__.__name__)
205206
self.all_cve_data = all_cve_data
@@ -213,6 +214,7 @@ def __init__(
213214
self.time_of_last_update = time_of_last_update
214215
self.append = append
215216
self.tag = tag
217+
self.merge_report = merge_report
216218

217219
def output_cves(self, outfile, output_type="console"):
218220
"""Output a list of CVEs
@@ -236,6 +238,7 @@ def output_cves(self, outfile, output_type="console"):
236238
self.total_files,
237239
self.products_with_cve,
238240
self.products_without_cve,
241+
self.merge_report,
239242
self.logger,
240243
outfile,
241244
)

0 commit comments

Comments
 (0)