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
116 changes: 115 additions & 1 deletion scripts/vulnerability_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
Retrieve issues
-g, --generate-graphs
Generate graphs with vulnerabilities info
-svg, --svg-output Generate graphs in SVG format
-png, --png-output Generate graphs in PNG format
-p, --generate-page Generate page with vulnerabilities info
-c, --comparison COMPARISON [COMPARISON ...]
Compare two or more repositories and generate
Expand All @@ -40,6 +42,9 @@

from dateutil import parser

import matplotlib.pyplot as plt
from matplotlib.figure import Figure

type DependabotAlert = dict[str, Any]
type DependabotAlerts = list[DependabotAlert]

Expand Down Expand Up @@ -94,6 +99,22 @@ def create_argument_parser() -> ArgumentParser:
help="Generate graphs with vulnerabilities info",
)

cli_parser.add_argument(
"-svg",
"--svg-output",
action="store_true",
default=False,
help="Generate graphs in SVG format",
)

cli_parser.add_argument(
"-png",
"--png-output",
action="store_true",
default=False,
help="Generate graphs in PNG format",
)

cli_parser.add_argument(
"-p",
"--generate-page",
Expand All @@ -115,6 +136,22 @@ def create_argument_parser() -> ArgumentParser:
return cli_parser


def check_args(args: Namespace) -> None:
"""Check the validity of all command line arguments.

Validate command-line argument consistency.

Ensures that if graph generation is enabled, at least one output format (SVG or PNG) is specified.

Raises:
ValueError: If graph generation is requested but neither SVG nor PNG output is selected.
"""
if args.generate_graphs:
# when graphs should be generated, output format need to be specified too
if not args.svg_output and not args.png_output:
raise "Graphs generation was requested: you need to select the PNG and/or SVG output."


def dependabot_file_name(args: Namespace) -> str:
"""Construct file name containing Dependabot alerts.

Expand Down Expand Up @@ -326,7 +363,7 @@ def fill_in_days_statistic(source_data: DependabotAlerts) -> dict[str, Any]:
days_stat: dict[str, Any] = {}
days_stat["days"] = days
# avoid division by zero
if not days:
if days:
days_stat["avg"] = sum(days) / len(days)
days_stat["median"] = median(days)
else:
Expand Down Expand Up @@ -361,6 +398,18 @@ def fill_in_cve_created_dates(source_data: DependabotAlerts) -> Counter[datetime
def process_dependabot_file(dependabot_file: str) -> dict[str, Any]:
"""
Compute vulnerability statistics from Dependabot alerts stored in a JSON file.

Loads alerts from the specified file and calculates statistics including total count,
state breakdown (open/fixed), severity distribution, fix latency metrics, vulnerable
package frequencies, and CVE detection timeline.

Parameters:
dependabot_file (str): Path to the JSON file containing Dependabot alerts.
prefix (str): Unused; retained for interface compatibility.

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

Expand All @@ -378,6 +427,70 @@ def process_dependabot_file(dependabot_file: str) -> dict[str, Any]:
return stat


def save_graph(
fig: Figure, prefix: str, postfix: str, svg_output: bool, png_output: bool
) -> None:
"""
Save a figure in SVG and/or PNG formats based on output flags.

Parameters:
prefix (str): Filename prefix
postfix (str): Filename suffix appended after the prefix and underscore
"""
if svg_output:
filename = f"{prefix}_{postfix}.svg"
plt.savefig(filename, format="svg")
if png_output:
filename = f"{prefix}_{postfix}.png"
plt.savefig(filename, format="png")


def generate_overal_state_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate a bar chart showing the distribution of vulnerability states.

Parameters:
stat (dict[str, Any]): Statistics dictionary with a "state" key containing alert counts.
prefix (str): Filename prefix for the saved graph.
svg_output (bool): Whether to save in SVG format.
png_output (bool): Whether to save in PNG format.
"""
fig, ax = plt.subplots()
D = stat["state"]
ax.bar(
range(len(D)), list(D.values()), align="center", color=["#c00000", "#00c000"]
)
ax.set_ylim(top=400)
ax.set_xticks(range(len(D)), list(D.keys()))
save_graph(fig, prefix, "state", svg_output, png_output)


def generate_severity_graph(
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
) -> None:
"""
Generate a bar chart of vulnerability severity distribution and save it in the specified format.

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["severity"]
ax.bar(
range(len(D)),
list(D.values()),
align="center",
color=["#c00000", "orange", "#e0e000", "#00c000"],
)
ax.set_xticks(range(len(D)), list(D.keys()))
save_graph(fig, prefix, "severity", svg_output, png_output)


def main() -> int:
"""
CLI entry point that retrieves Dependabot issues and produces Vulnerability report.
Expand All @@ -397,6 +510,7 @@ def main() -> int:
"""
cli_parser = create_argument_parser()
args = cli_parser.parse_args()
check_args(args)
dependabot_file = dependabot_file_name(args)
prefix = args.repository
stat = process_dependabot_file(dependabot_file)
Expand Down
Loading