Skip to content
Merged
Changes from all 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
103 changes: 100 additions & 3 deletions scripts/vulnerability_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,8 @@ def process_dependabot_file(dependabot_file: str) -> dict[str, Any]:

Returns:
dict[str, Any]: A dictionary with keys: "count", "state", "severity", "severities",
"days", "packages", and "dates", each containing the corresponding computed metrics.
"days", "packages", and "dates", each containing the
corresponding computed metrics.
"""
source_data = load_dependabot_file(dependabot_file)

Expand Down Expand Up @@ -491,6 +492,98 @@ def generate_severity_graph(
save_graph(fig, prefix, "severity", svg_output, png_output)


def generate_vuln_for_days_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate a histogram of the distribution of days required to fix vulnerabilities.

Parameters:
stat (dict[str, Any]): Statistics dictionary
prefix (str): Prefix for the output file name.
svg_output (bool): If true, save the graph as SVG.
png_output (bool): If true, save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["days"]["days"]
ax.hist(D, bins=30, edgecolor="black")
ax.set_ylim(top=100)
ax.set_xlabel("Days")
ax.set_title("Fixed in day(s)")
save_graph(fig, prefix, "days", svg_output, png_output)


def generate_vulnerable_packages_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate a bar chart showing the top 10 packages with the most CVEs.

Parameters:
stat (dict[str, Any]): Statistics dictionary containing vulnerability
data, with "packages" key holding package frequency counts.
prefix (str): Prefix for the output file name.
svg_output (bool): If true, save the graph as SVG.
png_output (bool): If true, save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["packages"]
names, counts = zip(*D.most_common(10))
ax.bar(names, counts, edgecolor="black")
ax.set_ylim(top=100)
ax.set_title("CVEs per package")
ax.tick_params(axis="x", labelrotation=90)
fig.tight_layout()
Comment on lines +507 to +536

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major

Remove the fixed top=100 y-axis limit.

The hard-coded ax.set_ylim(top=100) in both graph functions truncates data when vulnerability counts exceed 100. Popular packages frequently accumulate hundreds of CVEs, and large repositories produce histogram bin counts well above this threshold. This arbitrary cap renders charts misleading for datasets where the visualization is most critical. Letting matplotlib autoscale ensures accuracy across all data sizes.

Proposed fix
 def generate_vuln_for_days_graph(
     stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
 ) -> None:
     (...)
     D = stat["days"]["days"]
     ax.hist(D, bins=30, edgecolor="black")
-    ax.set_ylim(top=100)
     ax.set_xlabel("Days")
     ax.set_title("Fixed in day(s)")
     save_graph(fig, prefix, "days", svg_output, png_output)


 def generate_vulnerable_packages_graph(
     stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
 ) -> None:
     (...)
     D = stat["packages"]
     names, counts = zip(*D.most_common(10))
     ax.bar(names, counts, edgecolor="black")
-    ax.set_ylim(top=100)
     ax.set_title("CVEs per package")
     ax.tick_params(axis="x", labelrotation=90)
     fig.tight_layout()
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fig, ax = plt.subplots()
D = stat["days"]["days"]
ax.hist(D, bins=30, edgecolor="black")
ax.set_ylim(top=100)
ax.set_xlabel("Days")
ax.set_title("Fixed in day(s)")
save_graph(fig, prefix, "days", svg_output, png_output)
def generate_vulnerable_packages_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate a bar chart showing the top 10 packages with the most CVEs.
Parameters:
stat (dict[str, Any]): Statistics dictionary containing vulnerability
data, with "packages" key holding package frequency counts.
prefix (str): Prefix for the output file name.
svg_output (bool): If true, save the graph as SVG.
png_output (bool): If true, save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["packages"]
names, counts = zip(*D.most_common(10))
ax.bar(names, counts, edgecolor="black")
ax.set_ylim(top=100)
ax.set_title("CVEs per package")
ax.tick_params(axis="x", labelrotation=90)
fig.tight_layout()
fig, ax = plt.subplots()
D = stat["days"]["days"]
ax.hist(D, bins=30, edgecolor="black")
ax.set_xlabel("Days")
ax.set_title("Fixed in day(s)")
save_graph(fig, prefix, "days", svg_output, png_output)
def generate_vulnerable_packages_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate a bar chart showing the top 10 packages with the most CVEs.
Parameters:
stat (dict[str, Any]): Statistics dictionary containing vulnerability
data, with "packages" key holding package frequency counts.
prefix (str): Prefix for the output file name.
svg_output (bool): If true, save the graph as SVG.
png_output (bool): If true, save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["packages"]
names, counts = zip(*D.most_common(10))
ax.bar(names, counts, edgecolor="black")
ax.set_title("CVEs per package")
ax.tick_params(axis="x", labelrotation=90)
fig.tight_layout()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/vulnerability_report.py` around lines 507 - 536, The two plotting
helpers, generate_fixed_in_day_graph and generate_vulnerable_packages_graph, are
hard-capping the y-axis with ax.set_ylim(top=100), which can truncate high-count
data. Remove that fixed limit from both functions so matplotlib can autoscale
based on the actual values, while keeping the rest of the chart setup unchanged.

save_graph(fig, prefix, "packages", svg_output, png_output)


def generate_new_cve_dates_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Create a line chart showing new CVE detection dates over time.

Parameters:
stat (dict[str, Any]): Statistics dictionary with "dates" key
containing a Counter of datetime objects mapped to occurrence counts.
prefix (str): Base filename prefix for output files.
svg_output (bool): Whether to save the graph as SVG.
png_output (bool): Whether to save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["dates"]
dates, counts = zip(*sorted(D.items(), key=lambda x: x[0]))
ax.plot(dates, counts)
Comment on lines +529 to +556

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major

Fix empty-counter crash in graph generation helpers.

Lines 531 and 555 attempt to unpack zip(*) results without checking if the input Counter is empty. Since Counter.most_common(10) and sorted(Counter.items()) return empty lists when the counter has no items, zip(*[]) produces an empty iterator. Unpacking this into names, counts raises ValueError: not enough values to unpack, causing the CLI to crash on valid empty Dependabot reports when --generate-graphs is enabled.

Guard against empty data explicitly before unpacking to emit a "No data" placeholder graph instead.

Proposed fix
 def generate_vulnerable_packages_graph(
     stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
 ) -> None:
@@
     fig, ax = plt.subplots()
     D = stat["packages"]
+    if not D:
+        ax.set_title("CVEs per package")
+        ax.text(0.5, 0.5, "No data", ha="center", va="center", transform=ax.transAxes)
+        save_graph(fig, prefix, "packages", svg_output, png_output)
+        return
     names, counts = zip(*D.most_common(10))
@@
 def generate_new_cve_dates_graph(
     stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
 ) -> None:
@@
     fig, ax = plt.subplots()
     D = stat["dates"]
+    if not D:
+        ax.set_title("New CVEs timeline")
+        ax.text(0.5, 0.5, "No data", ha="center", va="center", transform=ax.transAxes)
+        save_graph(fig, prefix, "timeline", svg_output, png_output)
+        return
     dates, counts = zip(*sorted(D.items(), key=lambda x: x[0]))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fig, ax = plt.subplots()
D = stat["packages"]
names, counts = zip(*D.most_common(10))
ax.bar(names, counts, edgecolor="black")
ax.set_ylim(top=100)
ax.set_title("CVEs per package")
ax.tick_params(axis="x", labelrotation=90)
fig.tight_layout()
save_graph(fig, prefix, "packages", svg_output, png_output)
def generate_new_cve_dates_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Create a line chart showing new CVE detection dates over time.
Parameters:
stat (dict[str, Any]): Statistics dictionary with "dates" key
containing a Counter of datetime objects mapped to occurrence counts.
prefix (str): Base filename prefix for output files.
svg_output (bool): Whether to save the graph as SVG.
png_output (bool): Whether to save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["dates"]
dates, counts = zip(*sorted(D.items(), key=lambda x: x[0]))
ax.plot(dates, counts)
fig, ax = plt.subplots()
D = stat["packages"]
if not D:
ax.set_title("CVEs per package")
ax.text(0.5, 0.5, "No data", ha="center", va="center", transform=ax.transAxes)
save_graph(fig, prefix, "packages", svg_output, png_output)
return
names, counts = zip(*D.most_common(10))
ax.bar(names, counts, edgecolor="black")
ax.set_ylim(top=100)
ax.set_title("CVEs per package")
ax.tick_params(axis="x", labelrotation=90)
fig.tight_layout()
save_graph(fig, prefix, "packages", svg_output, png_output)
def generate_new_cve_dates_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Create a line chart showing new CVE detection dates over time.
Parameters:
stat (dict[str, Any]): Statistics dictionary with "dates" key
containing a Counter of datetime objects mapped to occurrence counts.
prefix (str): Base filename prefix for output files.
svg_output (bool): Whether to save the graph as SVG.
png_output (bool): Whether to save the graph as PNG.
"""
fig, ax = plt.subplots()
D = stat["dates"]
if not D:
ax.set_title("New CVEs timeline")
ax.text(0.5, 0.5, "No data", ha="center", va="center", transform=ax.transAxes)
save_graph(fig, prefix, "timeline", svg_output, png_output)
return
dates, counts = zip(*sorted(D.items(), key=lambda x: x[0]))
ax.plot(dates, counts)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/vulnerability_report.py` around lines 529 - 556, The graph helpers
crash when their Counter inputs are empty because generate_new_cve_dates_graph
and the package bar-chart block both unpack zip(*) results unconditionally.
Update the CVE dates and packages graph generation paths to detect empty data
before calling zip/unpacking, and in that case render the existing “No data”
placeholder instead of continuing. Use the generate_new_cve_dates_graph function
and the packages graph block that reads stat["dates"] and stat["packages"] as
the locations to apply the guard.

ax.set_title("New CVEs timeline")
save_graph(fig, prefix, "timeline", svg_output, png_output)


def generate_graphs(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate vulnerability visualization graphs.
"""
generate_overal_state_graph(stat, prefix, svg_output, png_output)
generate_severity_graph(stat, prefix, svg_output, png_output)
generate_vuln_for_days_graph(stat, prefix, svg_output, png_output)
generate_vulnerable_packages_graph(stat, prefix, svg_output, png_output)
generate_new_cve_dates_graph(stat, prefix, svg_output, png_output)


def generate_page(
stat: dict[str, Any], prefix: str, organization: str, repository: str
) -> None:
"""
Generate HTML page containing visualization graphs.
"""
filename = f"vulnerabilities_{prefix}.html"
output = f"""<!DOCTYPE html>
"""
with open(filename, "w", encoding="utf-8") as fout:
fout.write(output)


def main() -> int:
"""
CLI entry point that retrieves Dependabot issues and produces Vulnerability report.
Expand All @@ -514,8 +607,12 @@ def main() -> int:
dependabot_file = dependabot_file_name(args)
prefix = args.repository
stat = process_dependabot_file(dependabot_file)
print(stat)
print(prefix)

if args.generate_graphs:
generate_graphs(stat, prefix, args.svg_output, args.png_output)

if args.generate_page:
generate_page(stat, prefix, args.organization, args.repository)

return 0

Expand Down
Loading