Skip to content

Commit 5eb2aa6

Browse files
authored
feat: No-Scan changes for html reports (#5284)
Signed-off-by: joydeep049 <[email protected]>
1 parent e94fa59 commit 5eb2aa6

File tree

8 files changed

+295
-114
lines changed

8 files changed

+295
-114
lines changed

.github/workflows/update-js-dependencies.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ jobs:
7070
run: |
7171
python -c 'from test.test_output_engine import TestOutputEngine; \
7272
from cve_bin_tool.output_engine.html import output_html; \
73-
output_html(TestOutputEngine.MOCK_OUTPUT, None, "", "", "", 3, 3, 0, None, None, open("test.html", "w"))'
73+
output_html(TestOutputEngine.MOCK_OUTPUT, None, "", "", "", 3, 3, 0, None, None, open("test.html", "w"), no_scan=False)'
7474
7575
- name: Upload mock report
7676
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2

cve_bin_tool/output_engine/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -833,6 +833,7 @@ def output_cves(self, outfile, output_type="console"):
833833
outfile,
834834
self.affected_versions,
835835
self.strip_scan_dir,
836+
self.no_scan,
836837
)
837838
else: # console, or anything else that is unrecognised
838839
output_console(

cve_bin_tool/output_engine/html.py

Lines changed: 169 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def output_html(
9797
outfile,
9898
affected_versions: int = 0,
9999
strip_scan_dir: bool = False,
100+
no_scan: bool = False,
100101
):
101102
"""Returns a HTML report for CVE's"""
102103

@@ -156,15 +157,27 @@ def output_html(
156157
# Start generating graph with the data
157158

158159
# dash graph1: Products Vulnerability Graph
159-
product_pie = go.Figure(
160-
data=[
161-
go.Pie(
162-
labels=["Vulnerable", "No Known Vulnerability"],
163-
values=[products_with_cve, products_without_cve],
164-
hole=0.4,
165-
)
166-
]
167-
)
160+
if no_scan:
161+
# In no-scan mode, show detected products vs no products
162+
product_pie = go.Figure(
163+
data=[
164+
go.Pie(
165+
labels=["Detected Products", "No Products Detected"],
166+
values=[products_with_cve + products_without_cve, 0],
167+
hole=0.4,
168+
)
169+
]
170+
)
171+
else:
172+
product_pie = go.Figure(
173+
data=[
174+
go.Pie(
175+
labels=["Vulnerable", "No Known Vulnerability"],
176+
values=[products_with_cve, products_without_cve],
177+
hole=0.4,
178+
)
179+
]
180+
)
168181

169182
# Chart configuration for product_pie
170183
product_pie.update_layout(
@@ -183,46 +196,50 @@ def output_html(
183196

184197
# dash graph2: Product CVE's Graph
185198
cve_bar = go.Figure()
186-
data_by_cve_remarks: dict = {
187-
"NEW": {"x": [], "y": []},
188-
"MITIGATED": {"x": [], "y": []},
189-
"CONFIRMED": {"x": [], "y": []},
190-
"UNEXPLORED": {"x": [], "y": []},
191-
"FALSE POSITIVE": {"x": [], "y": []},
192-
"NOT AFFECTED": {"x": [], "y": []},
193-
}
194-
for product_info, cve_data in all_cve_data.items():
195-
# Check if product contains CVEs
196-
if cve_data["cves"]:
197-
cve_by_remark = group_cve_by_remark(cve_data["cves"])
198-
for key, val in [
199-
["NEW", len(cve_by_remark[Remarks.NewFound])],
200-
["MITIGATED", len(cve_by_remark[Remarks.Mitigated])],
201-
["CONFIRMED", len(cve_by_remark[Remarks.Confirmed])],
202-
["UNEXPLORED", len(cve_by_remark[Remarks.Unexplored])],
203-
["FALSE POSITIVE", len(cve_by_remark[Remarks.FalsePositive])],
204-
["NOT AFFECTED", len(cve_by_remark[Remarks.NotAffected])],
205-
]:
206-
x = (
207-
f"{product_info.vendor}-{product_info.product}({product_info.version})"
208-
if product_info.vendor != "UNKNOWN"
209-
else f"{product_info.product}({product_info.version})"
199+
if not no_scan:
200+
data_by_cve_remarks: dict = {
201+
"NEW": {"x": [], "y": []},
202+
"MITIGATED": {"x": [], "y": []},
203+
"CONFIRMED": {"x": [], "y": []},
204+
"UNEXPLORED": {"x": [], "y": []},
205+
"FALSE POSITIVE": {"x": [], "y": []},
206+
"NOT AFFECTED": {"x": [], "y": []},
207+
}
208+
for product_info, cve_data in all_cve_data.items():
209+
# Check if product contains CVEs
210+
if cve_data["cves"]:
211+
cve_by_remark = group_cve_by_remark(cve_data["cves"])
212+
for key, val in [
213+
["NEW", len(cve_by_remark[Remarks.NewFound])],
214+
["MITIGATED", len(cve_by_remark[Remarks.Mitigated])],
215+
["CONFIRMED", len(cve_by_remark[Remarks.Confirmed])],
216+
["UNEXPLORED", len(cve_by_remark[Remarks.Unexplored])],
217+
["FALSE POSITIVE", len(cve_by_remark[Remarks.FalsePositive])],
218+
["NOT AFFECTED", len(cve_by_remark[Remarks.NotAffected])],
219+
]:
220+
x = (
221+
f"{product_info.vendor}-{product_info.product}({product_info.version})"
222+
if product_info.vendor != "UNKNOWN"
223+
else f"{product_info.product}({product_info.version})"
224+
)
225+
y = 0 if cve_data["cves"][0][1] == "UNKNOWN" else val
226+
data_by_cve_remarks[key]["x"].append(x)
227+
data_by_cve_remarks[key]["y"].append(y)
228+
229+
for key, val in data_by_cve_remarks.items():
230+
cve_bar.add_trace(
231+
go.Bar(
232+
x=val["x"],
233+
y=val["y"],
234+
name=key,
210235
)
211-
y = 0 if cve_data["cves"][0][1] == "UNKNOWN" else val
212-
data_by_cve_remarks[key]["x"].append(x)
213-
data_by_cve_remarks[key]["y"].append(y)
214-
215-
for key, val in data_by_cve_remarks.items():
216-
cve_bar.add_trace(
217-
go.Bar(
218-
x=val["x"],
219-
y=val["y"],
220-
name=key,
221236
)
222-
)
223237

224238
# Chart configuration for cve_bar
225-
cve_bar.update_layout(yaxis_title="Number of CVE's", barmode="stack")
239+
if no_scan:
240+
cve_bar.update_layout(yaxis_title="No CVE Analysis Performed", barmode="stack")
241+
else:
242+
cve_bar.update_layout(yaxis_title="Number of CVE's", barmode="stack")
226243

227244
all_paths = defaultdict(list)
228245

@@ -240,56 +257,71 @@ def output_html(
240257
cve_severity = {"CRITICAL": 0, "HIGH": 0, "MEDIUM": 0, "LOW": 0, "UNKNOWN": 0}
241258

242259
cve_by_metrics: defaultdict[Remarks, list[dict[str, str]]] = defaultdict(list)
243-
for product_info, cve_data in all_cve_data.items():
244-
if cve_data["cves"]:
245-
for cve in cve_data["cves"]:
246-
probability = "-"
247-
percentile = "-"
248-
249-
for metric, field in cve.metric.items():
250-
if metric == "EPSS":
251-
probability = round(field[0] * 100, 4)
252-
percentile = field[1]
253-
254-
cve_by_metrics[cve.remarks].append(
255-
{
256-
"cve_number": cve.cve_number,
257-
"cvss_version": str(cve.cvss_version),
258-
"cvss_score": str(cve.score),
259-
"epss_probability": str(probability),
260-
"epss_percentile": str(percentile),
261-
"severity": cve.severity,
262-
}
263-
)
260+
if not no_scan:
261+
for product_info, cve_data in all_cve_data.items():
262+
if cve_data["cves"]:
263+
for cve in cve_data["cves"]:
264+
probability = "-"
265+
percentile = "-"
266+
267+
for metric, field in cve.metric.items():
268+
if metric == "EPSS":
269+
probability = round(field[0] * 100, 4)
270+
percentile = field[1]
271+
272+
cve_by_metrics[cve.remarks].append(
273+
{
274+
"cve_number": cve.cve_number,
275+
"cvss_version": str(cve.cvss_version),
276+
"cvss_score": str(cve.score),
277+
"epss_probability": str(probability),
278+
"epss_percentile": str(percentile),
279+
"severity": cve.severity,
280+
}
281+
)
264282

265283
cve_metric_html_rows = []
266-
for remarks in sorted(cve_by_metrics):
267-
for cve in cve_by_metrics[remarks]:
268-
row_color = "table-success"
269-
if cve["severity"] == "CRITICAL":
270-
row_color = "table-danger"
271-
elif cve["severity"] == "HIGH":
272-
row_color = "table-primary"
273-
elif cve["severity"] == "MEDIUM":
274-
row_color = "table-warning"
275-
276-
html_row = f"""
277-
<tr class="{row_color}">
278-
<th scope="row">{cve["cve_number"]}</th>
279-
<td>{cve["cvss_version"]}</td>
280-
<td>{cve["cvss_score"]}</td>
281-
<td>{cve["epss_probability"]}</td>
282-
<td>{cve["epss_percentile"]}</td>
283-
</tr>
284-
"""
285-
cve_metric_html_rows.append(html_row)
284+
if not no_scan:
285+
for remarks in sorted(cve_by_metrics):
286+
for cve in cve_by_metrics[remarks]:
287+
row_color = "table-success"
288+
if cve["severity"] == "CRITICAL":
289+
row_color = "table-danger"
290+
elif cve["severity"] == "HIGH":
291+
row_color = "table-primary"
292+
elif cve["severity"] == "MEDIUM":
293+
row_color = "table-warning"
294+
295+
html_row = f"""
296+
<tr class="{row_color}">
297+
<th scope="row">{cve["cve_number"]}</th>
298+
<td>{cve["cvss_version"]}</td>
299+
<td>{cve["cvss_score"]}</td>
300+
<td>{cve["epss_probability"]}</td>
301+
<td>{cve["epss_percentile"]}</td>
302+
</tr>
303+
"""
304+
cve_metric_html_rows.append(html_row)
286305
# Join the HTML rows to create the full table content
287306
table_content = "\n".join(cve_metric_html_rows)
288307

289308
# List of Products
290309
for product_info, cve_data in all_cve_data.items():
291-
# Check if product contains CVEs
292-
if cve_data["cves"]:
310+
# hid is unique for each product
311+
if product_info.vendor != "UNKNOWN":
312+
hid = f"{product_info.vendor}{product_info.product}{''.join(product_info.version.split('.'))}"
313+
else:
314+
hid = f"{product_info.product}{''.join(product_info.version.split('.'))}"
315+
316+
if strip_scan_dir:
317+
product_paths = [
318+
strip_path(path, scanned_dir) for path in cve_data["paths"]
319+
]
320+
else:
321+
product_paths = cve_data["paths"]
322+
323+
if not no_scan and cve_data["cves"]:
324+
# Process products with CVEs in normal scan mode
293325
# group product wise cves on the basis of remarks
294326
cve_by_remark = group_cve_by_remark(cve_data["cves"])
295327

@@ -304,13 +336,6 @@ def output_html(
304336
norm_severity = normalize_severity(cve.severity)
305337
cve_severity[norm_severity] += 1
306338

307-
# hid is unique for each product
308-
if product_info.vendor != "UNKNOWN":
309-
hid = f"{product_info.vendor}{product_info.product}{''.join(product_info.version.split('.'))}"
310-
else:
311-
hid = (
312-
f"{product_info.product}{''.join(product_info.version.split('.'))}"
313-
)
314339
new_cves = render_cves(
315340
hid,
316341
cve_row,
@@ -404,13 +429,6 @@ def output_html(
404429
if not_affected_cves:
405430
remarks += "not_affected "
406431

407-
if strip_scan_dir:
408-
product_paths = [
409-
strip_path(path, scanned_dir) for path in cve_data["paths"]
410-
]
411-
else:
412-
product_paths = cve_data["paths"]
413-
414432
products_found.append(
415433
product_row.render(
416434
vendor=product_info.vendor,
@@ -432,13 +450,56 @@ def output_html(
432450
not_affected_cves=not_affected_cves,
433451
)
434452
)
453+
else:
454+
# Process products in no-scan mode or products without CVEs
455+
if no_scan:
456+
remarks = "no_scan"
457+
cve_count = 0
458+
severity_analysis = ""
459+
new_cves = ""
460+
mitigated_cves = ""
461+
confirmed_cves = ""
462+
unexplored_cves = ""
463+
false_positive_cves = ""
464+
not_affected_cves = ""
465+
else:
466+
# Products without CVEs in normal scan mode
467+
remarks = "no_cves"
468+
cve_count = 0
469+
severity_analysis = ""
470+
new_cves = ""
471+
mitigated_cves = ""
472+
confirmed_cves = ""
473+
unexplored_cves = ""
474+
false_positive_cves = ""
475+
not_affected_cves = ""
476+
477+
products_found.append(
478+
product_row.render(
479+
vendor=product_info.vendor,
480+
name=product_info.product,
481+
version=product_info.version,
482+
cve_count=cve_count,
483+
severity_analysis=severity_analysis,
484+
remarks=remarks,
485+
fix_id=hid,
486+
paths=product_paths,
487+
len_paths=len(product_paths),
488+
new_cves=new_cves,
489+
mitigated_cves=mitigated_cves,
490+
confirmed_cves=confirmed_cves,
491+
unexplored_cves=unexplored_cves,
492+
false_positive_cves=false_positive_cves,
493+
not_affected_cves=not_affected_cves,
494+
)
495+
)
435496

436-
if "*" in product_info.vendor:
437-
star_warn = "* vendors guessed by the tool"
497+
if "*" in product_info.vendor:
498+
star_warn = "* vendors guessed by the tool"
438499

439-
# update all_paths
440-
for path in product_paths:
441-
all_paths[path].append(hid)
500+
# update all_paths
501+
for path in product_paths:
502+
all_paths[path].append(hid)
442503

443504
# Dashboard Rendering
444505
dashboard = dashboard.render(
@@ -451,6 +512,7 @@ def output_html(
451512
cve_remarks=cve_remarks,
452513
cve_severity=cve_severity,
453514
table_content=table_content,
515+
no_scan=no_scan,
454516
)
455517

456518
# try to load the bigger files just before the generation of report

0 commit comments

Comments
 (0)