Skip to content

Commit 2ec88d9

Browse files
committed
LCORE-2631: Plot graphs
1 parent be84030 commit 2ec88d9

1 file changed

Lines changed: 115 additions & 1 deletion

File tree

scripts/vulnerability_report.py

Lines changed: 115 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
Retrieve issues
2525
-g, --generate-graphs
2626
Generate graphs with vulnerabilities info
27+
-svg, --svg-output Generate graphs in SVG format
28+
-png, --png-output Generate graphs in PNG format
2729
-p, --generate-page Generate page with vulnerabilities info
2830
-c, --comparison COMPARISON [COMPARISON ...]
2931
Compare two or more repositories and generate
@@ -40,6 +42,9 @@
4042

4143
from dateutil import parser
4244

45+
import matplotlib.pyplot as plt
46+
from matplotlib.figure import Figure
47+
4348
type DependabotAlert = dict[str, Any]
4449
type DependabotAlerts = list[DependabotAlert]
4550

@@ -94,6 +99,22 @@ def create_argument_parser() -> ArgumentParser:
9499
help="Generate graphs with vulnerabilities info",
95100
)
96101

102+
cli_parser.add_argument(
103+
"-svg",
104+
"--svg-output",
105+
action="store_true",
106+
default=False,
107+
help="Generate graphs in SVG format",
108+
)
109+
110+
cli_parser.add_argument(
111+
"-png",
112+
"--png-output",
113+
action="store_true",
114+
default=False,
115+
help="Generate graphs in PNG format",
116+
)
117+
97118
cli_parser.add_argument(
98119
"-p",
99120
"--generate-page",
@@ -115,6 +136,22 @@ def create_argument_parser() -> ArgumentParser:
115136
return cli_parser
116137

117138

139+
def check_args(args: Namespace) -> None:
140+
"""Check the validity of all command line arguments.
141+
142+
Validate command-line argument consistency.
143+
144+
Ensures that if graph generation is enabled, at least one output format (SVG or PNG) is specified.
145+
146+
Raises:
147+
ValueError: If graph generation is requested but neither SVG nor PNG output is selected.
148+
"""
149+
if args.generate_graphs:
150+
# when graphs should be generated, output format need to be specified too
151+
if not args.svg_output and not args.png_output:
152+
raise "Graphs generation was requested: you need to select the PNG and/or SVG output."
153+
154+
118155
def dependabot_file_name(args: Namespace) -> str:
119156
"""Construct file name containing Dependabot alerts.
120157
@@ -326,7 +363,7 @@ def fill_in_days_statistic(source_data: DependabotAlerts) -> dict[str, Any]:
326363
days_stat: dict[str, Any] = {}
327364
days_stat["days"] = days
328365
# avoid division by zero
329-
if not days:
366+
if days:
330367
days_stat["avg"] = sum(days) / len(days)
331368
days_stat["median"] = median(days)
332369
else:
@@ -361,6 +398,18 @@ def fill_in_cve_created_dates(source_data: DependabotAlerts) -> Counter[datetime
361398
def process_dependabot_file(dependabot_file: str) -> dict[str, Any]:
362399
"""
363400
Compute vulnerability statistics from Dependabot alerts stored in a JSON file.
401+
402+
Loads alerts from the specified file and calculates statistics including total count,
403+
state breakdown (open/fixed), severity distribution, fix latency metrics, vulnerable
404+
package frequencies, and CVE detection timeline.
405+
406+
Parameters:
407+
dependabot_file (str): Path to the JSON file containing Dependabot alerts.
408+
prefix (str): Unused; retained for interface compatibility.
409+
410+
Returns:
411+
dict[str, Any]: A dictionary with keys: "count", "state", "severity", "severities",
412+
"days", "packages", and "dates", each containing the corresponding computed metrics.
364413
"""
365414
source_data = load_dependabot_file(dependabot_file)
366415

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

380429

430+
def save_graph(
431+
fig: Figure, prefix: str, postfix: str, svg_output: bool, png_output: bool
432+
) -> None:
433+
"""
434+
Save a figure in SVG and/or PNG formats based on output flags.
435+
436+
Parameters:
437+
prefix (str): Filename prefix
438+
postfix (str): Filename suffix appended after the prefix and underscore
439+
"""
440+
if svg_output:
441+
filename = f"{prefix}_{postfix}.svg"
442+
plt.savefig(filename, format="svg")
443+
if png_output:
444+
filename = f"{prefix}_{postfix}.png"
445+
plt.savefig(filename, format="png")
446+
447+
448+
def generate_overal_state_graph(
449+
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
450+
) -> None:
451+
"""
452+
Generate a bar chart showing the distribution of vulnerability states.
453+
454+
Parameters:
455+
stat (dict[str, Any]): Statistics dictionary with a "state" key containing alert counts.
456+
prefix (str): Filename prefix for the saved graph.
457+
svg_output (bool): Whether to save in SVG format.
458+
png_output (bool): Whether to save in PNG format.
459+
"""
460+
fig, ax = plt.subplots()
461+
D = stat["state"]
462+
ax.bar(
463+
range(len(D)), list(D.values()), align="center", color=["#c00000", "#00c000"]
464+
)
465+
ax.set_ylim(top=400)
466+
ax.set_xticks(range(len(D)), list(D.keys()))
467+
save_graph(fig, prefix, "state", svg_output, png_output)
468+
469+
470+
def generate_severity_graph(
471+
stat: dict[str, Any], prefix: str, svg_output: bool, png_output: bool
472+
) -> None:
473+
"""
474+
Generate a bar chart of vulnerability severity distribution and save it in the specified format.
475+
476+
Parameters:
477+
stat (dict[str, Any]): Statistics dictionary
478+
prefix (str): Prefix for the output file name.
479+
svg_output (bool): If true, save the graph as SVG.
480+
png_output (bool): If true, save the graph as PNG.
481+
"""
482+
fig, ax = plt.subplots()
483+
D = stat["severity"]
484+
ax.bar(
485+
range(len(D)),
486+
list(D.values()),
487+
align="center",
488+
color=["#c00000", "orange", "#e0e000", "#00c000"],
489+
)
490+
ax.set_xticks(range(len(D)), list(D.keys()))
491+
save_graph(fig, prefix, "severity", svg_output, png_output)
492+
493+
381494
def main() -> int:
382495
"""
383496
CLI entry point that retrieves Dependabot issues and produces Vulnerability report.
@@ -397,6 +510,7 @@ def main() -> int:
397510
"""
398511
cli_parser = create_argument_parser()
399512
args = cli_parser.parse_args()
513+
check_args(args)
400514
dependabot_file = dependabot_file_name(args)
401515
prefix = args.repository
402516
stat = process_dependabot_file(dependabot_file)

0 commit comments

Comments
 (0)