Skip to content

feat: No-Scan changes for html reports #5284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/update-js-dependencies.yml
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ jobs:
run: |
python -c 'from test.test_output_engine import TestOutputEngine; \
from cve_bin_tool.output_engine.html import output_html; \
output_html(TestOutputEngine.MOCK_OUTPUT, None, "", "", "", 3, 3, 0, None, None, open("test.html", "w"))'
output_html(TestOutputEngine.MOCK_OUTPUT, None, "", "", "", 3, 3, 0, None, None, open("test.html", "w"), no_scan=False)'

- name: Upload mock report
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
Expand Down
1 change: 1 addition & 0 deletions cve_bin_tool/output_engine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,6 +799,7 @@ def output_cves(self, outfile, output_type="console"):
outfile,
self.affected_versions,
self.strip_scan_dir,
self.no_scan,
)
else: # console, or anything else that is unrecognised
output_console(
Expand Down
276 changes: 169 additions & 107 deletions cve_bin_tool/output_engine/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ def output_html(
outfile,
affected_versions: int = 0,
strip_scan_dir: bool = False,
no_scan: bool = False,
):
"""Returns a HTML report for CVE's"""

Expand Down Expand Up @@ -156,15 +157,27 @@ def output_html(
# Start generating graph with the data

# dash graph1: Products Vulnerability Graph
product_pie = go.Figure(
data=[
go.Pie(
labels=["Vulnerable", "No Known Vulnerability"],
values=[products_with_cve, products_without_cve],
hole=0.4,
)
]
)
if no_scan:
# In no-scan mode, show detected products vs no products
product_pie = go.Figure(
data=[
go.Pie(
labels=["Detected Products", "No Products Detected"],
values=[products_with_cve + products_without_cve, 0],
hole=0.4,
)
]
)
else:
product_pie = go.Figure(
data=[
go.Pie(
labels=["Vulnerable", "No Known Vulnerability"],
values=[products_with_cve, products_without_cve],
hole=0.4,
)
]
)

# Chart configuration for product_pie
product_pie.update_layout(
Expand All @@ -183,46 +196,50 @@ def output_html(

# dash graph2: Product CVE's Graph
cve_bar = go.Figure()
data_by_cve_remarks: dict = {
"NEW": {"x": [], "y": []},
"MITIGATED": {"x": [], "y": []},
"CONFIRMED": {"x": [], "y": []},
"UNEXPLORED": {"x": [], "y": []},
"FALSE POSITIVE": {"x": [], "y": []},
"NOT AFFECTED": {"x": [], "y": []},
}
for product_info, cve_data in all_cve_data.items():
# Check if product contains CVEs
if cve_data["cves"]:
cve_by_remark = group_cve_by_remark(cve_data["cves"])
for key, val in [
["NEW", len(cve_by_remark[Remarks.NewFound])],
["MITIGATED", len(cve_by_remark[Remarks.Mitigated])],
["CONFIRMED", len(cve_by_remark[Remarks.Confirmed])],
["UNEXPLORED", len(cve_by_remark[Remarks.Unexplored])],
["FALSE POSITIVE", len(cve_by_remark[Remarks.FalsePositive])],
["NOT AFFECTED", len(cve_by_remark[Remarks.NotAffected])],
]:
x = (
f"{product_info.vendor}-{product_info.product}({product_info.version})"
if product_info.vendor != "UNKNOWN"
else f"{product_info.product}({product_info.version})"
if not no_scan:
data_by_cve_remarks: dict = {
"NEW": {"x": [], "y": []},
"MITIGATED": {"x": [], "y": []},
"CONFIRMED": {"x": [], "y": []},
"UNEXPLORED": {"x": [], "y": []},
"FALSE POSITIVE": {"x": [], "y": []},
"NOT AFFECTED": {"x": [], "y": []},
}
for product_info, cve_data in all_cve_data.items():
# Check if product contains CVEs
if cve_data["cves"]:
cve_by_remark = group_cve_by_remark(cve_data["cves"])
for key, val in [
["NEW", len(cve_by_remark[Remarks.NewFound])],
["MITIGATED", len(cve_by_remark[Remarks.Mitigated])],
["CONFIRMED", len(cve_by_remark[Remarks.Confirmed])],
["UNEXPLORED", len(cve_by_remark[Remarks.Unexplored])],
["FALSE POSITIVE", len(cve_by_remark[Remarks.FalsePositive])],
["NOT AFFECTED", len(cve_by_remark[Remarks.NotAffected])],
]:
x = (
f"{product_info.vendor}-{product_info.product}({product_info.version})"
if product_info.vendor != "UNKNOWN"
else f"{product_info.product}({product_info.version})"
)
y = 0 if cve_data["cves"][0][1] == "UNKNOWN" else val
data_by_cve_remarks[key]["x"].append(x)
data_by_cve_remarks[key]["y"].append(y)

for key, val in data_by_cve_remarks.items():
cve_bar.add_trace(
go.Bar(
x=val["x"],
y=val["y"],
name=key,
)
y = 0 if cve_data["cves"][0][1] == "UNKNOWN" else val
data_by_cve_remarks[key]["x"].append(x)
data_by_cve_remarks[key]["y"].append(y)

for key, val in data_by_cve_remarks.items():
cve_bar.add_trace(
go.Bar(
x=val["x"],
y=val["y"],
name=key,
)
)

# Chart configuration for cve_bar
cve_bar.update_layout(yaxis_title="Number of CVE's", barmode="stack")
if no_scan:
cve_bar.update_layout(yaxis_title="No CVE Analysis Performed", barmode="stack")
else:
cve_bar.update_layout(yaxis_title="Number of CVE's", barmode="stack")

all_paths = defaultdict(list)

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

cve_by_metrics: defaultdict[Remarks, list[dict[str, str]]] = defaultdict(list)
for product_info, cve_data in all_cve_data.items():
if cve_data["cves"]:
for cve in cve_data["cves"]:
probability = "-"
percentile = "-"

for metric, field in cve.metric.items():
if metric == "EPSS":
probability = round(field[0] * 100, 4)
percentile = field[1]

cve_by_metrics[cve.remarks].append(
{
"cve_number": cve.cve_number,
"cvss_version": str(cve.cvss_version),
"cvss_score": str(cve.score),
"epss_probability": str(probability),
"epss_percentile": str(percentile),
"severity": cve.severity,
}
)
if not no_scan:
for product_info, cve_data in all_cve_data.items():
if cve_data["cves"]:
for cve in cve_data["cves"]:
probability = "-"
percentile = "-"

for metric, field in cve.metric.items():
if metric == "EPSS":
probability = round(field[0] * 100, 4)
percentile = field[1]

cve_by_metrics[cve.remarks].append(
{
"cve_number": cve.cve_number,
"cvss_version": str(cve.cvss_version),
"cvss_score": str(cve.score),
"epss_probability": str(probability),
"epss_percentile": str(percentile),
"severity": cve.severity,
}
)

cve_metric_html_rows = []
for remarks in sorted(cve_by_metrics):
for cve in cve_by_metrics[remarks]:
row_color = "table-success"
if cve["severity"] == "CRITICAL":
row_color = "table-danger"
elif cve["severity"] == "HIGH":
row_color = "table-primary"
elif cve["severity"] == "MEDIUM":
row_color = "table-warning"

html_row = f"""
<tr class="{row_color}">
<th scope="row">{cve["cve_number"]}</th>
<td>{cve["cvss_version"]}</td>
<td>{cve["cvss_score"]}</td>
<td>{cve["epss_probability"]}</td>
<td>{cve["epss_percentile"]}</td>
</tr>
"""
cve_metric_html_rows.append(html_row)
if not no_scan:
for remarks in sorted(cve_by_metrics):
for cve in cve_by_metrics[remarks]:
row_color = "table-success"
if cve["severity"] == "CRITICAL":
row_color = "table-danger"
elif cve["severity"] == "HIGH":
row_color = "table-primary"
elif cve["severity"] == "MEDIUM":
row_color = "table-warning"

html_row = f"""
<tr class="{row_color}">
<th scope="row">{cve["cve_number"]}</th>
<td>{cve["cvss_version"]}</td>
<td>{cve["cvss_score"]}</td>
<td>{cve["epss_probability"]}</td>
<td>{cve["epss_percentile"]}</td>
</tr>
"""
cve_metric_html_rows.append(html_row)
# Join the HTML rows to create the full table content
table_content = "\n".join(cve_metric_html_rows)

# List of Products
for product_info, cve_data in all_cve_data.items():
# Check if product contains CVEs
if cve_data["cves"]:
# hid is unique for each product
if product_info.vendor != "UNKNOWN":
hid = f"{product_info.vendor}{product_info.product}{''.join(product_info.version.split('.'))}"
else:
hid = f"{product_info.product}{''.join(product_info.version.split('.'))}"

if strip_scan_dir:
product_paths = [
strip_path(path, scanned_dir) for path in cve_data["paths"]
]
else:
product_paths = cve_data["paths"]

if not no_scan and cve_data["cves"]:
# Process products with CVEs in normal scan mode
# group product wise cves on the basis of remarks
cve_by_remark = group_cve_by_remark(cve_data["cves"])

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

# hid is unique for each product
if product_info.vendor != "UNKNOWN":
hid = f"{product_info.vendor}{product_info.product}{''.join(product_info.version.split('.'))}"
else:
hid = (
f"{product_info.product}{''.join(product_info.version.split('.'))}"
)
new_cves = render_cves(
hid,
cve_row,
Expand Down Expand Up @@ -404,13 +429,6 @@ def output_html(
if not_affected_cves:
remarks += "not_affected "

if strip_scan_dir:
product_paths = [
strip_path(path, scanned_dir) for path in cve_data["paths"]
]
else:
product_paths = cve_data["paths"]

products_found.append(
product_row.render(
vendor=product_info.vendor,
Expand All @@ -432,13 +450,56 @@ def output_html(
not_affected_cves=not_affected_cves,
)
)
else:
# Process products in no-scan mode or products without CVEs
if no_scan:
remarks = "no_scan"
cve_count = 0
severity_analysis = ""
new_cves = ""
mitigated_cves = ""
confirmed_cves = ""
unexplored_cves = ""
false_positive_cves = ""
not_affected_cves = ""
else:
# Products without CVEs in normal scan mode
remarks = "no_cves"
cve_count = 0
severity_analysis = ""
new_cves = ""
mitigated_cves = ""
confirmed_cves = ""
unexplored_cves = ""
false_positive_cves = ""
not_affected_cves = ""

products_found.append(
product_row.render(
vendor=product_info.vendor,
name=product_info.product,
version=product_info.version,
cve_count=cve_count,
severity_analysis=severity_analysis,
remarks=remarks,
fix_id=hid,
paths=product_paths,
len_paths=len(product_paths),
new_cves=new_cves,
mitigated_cves=mitigated_cves,
confirmed_cves=confirmed_cves,
unexplored_cves=unexplored_cves,
false_positive_cves=false_positive_cves,
not_affected_cves=not_affected_cves,
)
)

if "*" in product_info.vendor:
star_warn = "* vendors guessed by the tool"
if "*" in product_info.vendor:
star_warn = "* vendors guessed by the tool"

# update all_paths
for path in product_paths:
all_paths[path].append(hid)
# update all_paths
for path in product_paths:
all_paths[path].append(hid)

# Dashboard Rendering
dashboard = dashboard.render(
Expand All @@ -451,6 +512,7 @@ def output_html(
cve_remarks=cve_remarks,
cve_severity=cve_severity,
table_content=table_content,
no_scan=no_scan,
)

# try to load the bigger files just before the generation of report
Expand Down
Loading
Loading