From 57b9ba4d14779b5a494f7119dfef808716e8c6b5 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 2 Nov 2026 08:37:01 +0100 Subject: [PATCH 1/5] Enhancement: display average number of new CVEs per day --- scripts/vulnerability_report.py | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 1026a7ae1..468167f6c 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -43,6 +43,7 @@ from typing import Any import matplotlib.pyplot as plt +import pandas as pd import requests from dateutil import parser @@ -540,7 +541,7 @@ def generate_vuln_for_days_graph( _, ax = plt.subplots() data = stat["days"]["days"] ax.hist(data, bins=30, edgecolor="black") - ax.set_ylim(top=100) + ax.set_ylim(top=150) ax.set_xlabel("Days") ax.set_title("Fixed in day(s)") save_graph(prefix, "days", svg_output, png_output) @@ -578,6 +579,7 @@ def generate_new_cve_dates_graph( 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. @@ -585,11 +587,30 @@ def generate_new_cve_dates_graph( png_output (bool): Whether to save the graph as PNG. """ - _, ax = plt.subplots() + _, ax = plt.subplots(figsize=(14, 7)) + data = stat["dates"] dates, counts = zip(*sorted(data.items(), key=lambda x: x[0])) - ax.plot(dates, counts) + ax.plot(dates, counts, label="CVEs per day") ax.set_title("New CVEs timeline") + s = pd.Series(counts, index=pd.to_datetime(list(dates))) + ma = s.rolling(window=7, min_periods=1, center=True).mean() + ax.plot(ma.index, ma.values, label="7-day moving average", linewidth=2) + + # horizontal line with "total" average + avg = s.mean() + ax.axhline( + avg, + color="red", + linestyle="--", + linewidth=1.5, + label=f"Day average ({avg:.2f})", + ) + + ax.legend() + + plt.setp(ax.get_xticklabels(), rotation=45, ha="right") + plt.subplots_adjust(bottom=0.20) save_graph(prefix, "timeline", svg_output, png_output) From 4cb61fe3677f2c6447675e4d96f50552d88e6d22 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 2 Nov 2026 08:37:40 +0100 Subject: [PATCH 2/5] Fixed message + refactored the processing function --- scripts/vulnerability_report.py | 63 +++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 468167f6c..bdd7ac2f5 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -754,6 +754,12 @@ def retrieve_issues(organization: str, repository: str, dependabot_file: str) -> print("GITHUB_TOKEN environment variable must be set!") return 1 + print("Retrieving issues:") + print(f" organization: {organization}") + print(f" repository: {repository}") + print(f" output file: {dependabot_file}") + + #url = f"https://api.github.com/repos/{organization}/{repository}/dependabot/alerts?state=open" url = f"https://api.github.com/repos/{organization}/{repository}/dependabot/alerts" headers = { @@ -793,12 +799,43 @@ def retrieve_issues(organization: str, repository: str, dependabot_file: str) -> f.write("]\n") raise - print("Saved all alerts to dependabot_alerts.json") + print(f"Saved all alerts to {dependabot_file}") # everything's ok return 0 +def process_issues_for_repository( + organization: str, + repository: str, + dependabot_file: str, + svg_output: bool, + png_output: bool, + retrieve_issues_operation: bool, + generate_graphs_operation: bool, + generate_page_operation: bool, +) -> int: + """Retrieve alerts or process the local file and generate selected outputs. + + Retrieval takes precedence over report generation and returns its status. + Otherwise return 0 after processing; file, data, and plotting errors propagate. + """ + prefix = repository + if retrieve_issues_operation: + # just retrieve issues, no other operation will be performed + return retrieve_issues(organization, repository, dependabot_file) + + stat = process_dependabot_file(dependabot_file) + + if generate_graphs_operation: + generate_graphs(stat, prefix, svg_output, png_output) + + if generate_page_operation: + generate_page(stat, prefix, organization, repository) + + return 0 + + def main() -> int: """ CLI entry point that retrieves Dependabot issues and produces Vulnerability report. @@ -831,21 +868,17 @@ def main() -> int: args = cli_parser.parse_args() check_args(args) dependabot_file = dependabot_file_name(args) - prefix = args.repository - - if args.retrieve_issues: - # just retrieve issues, no other operation will be performed - return retrieve_issues(args.organization, args.repository, dependabot_file) - - stat = process_dependabot_file(dependabot_file) - 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 + return process_issues_for_repository( + args.organization, + args.repository, + dependabot_file, + args.svg_output, + args.png_output, + args.retrieve_issues, + args.generate_graphs, + args.generate_page, + ) if __name__ == "__main__": From 9defbf352d64a6e824fa2e3d7e4a44289f9659b9 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 2 Nov 2026 08:38:51 +0100 Subject: [PATCH 3/5] Ability to process CVEs for all supported repositories --- scripts/vulnerability_report.py | 58 +++++++++++++++++++++++++-------- 1 file changed, 44 insertions(+), 14 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index bdd7ac2f5..21b4ba64e 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -22,6 +22,7 @@ GitHub repository. -r, --retrieve-issues Retrieve issues + -a, --all-actions Retrieve issues for all LCore repositories, generates graphs and reports -g, --generate-graphs Generate graphs with vulnerabilities info -svg, --svg-output Generate graphs in SVG format @@ -94,6 +95,14 @@ def create_argument_parser() -> ArgumentParser: help="Retrieve issues", ) + cli_parser.add_argument( + "-a", + "--all-actions", + action="store_true", + default=False, + help="Retrieve issues for all LCore repositories, generates graphs and reports", + ) + cli_parser.add_argument( "-g", "--generate-graphs", @@ -822,8 +831,8 @@ def process_issues_for_repository( """ prefix = repository if retrieve_issues_operation: - # just retrieve issues, no other operation will be performed - return retrieve_issues(organization, repository, dependabot_file) + # retrieve issues + retrieve_issues(organization, repository, dependabot_file) stat = process_dependabot_file(dependabot_file) @@ -867,18 +876,39 @@ def main() -> int: cli_parser = create_argument_parser() args = cli_parser.parse_args() check_args(args) - dependabot_file = dependabot_file_name(args) - - return process_issues_for_repository( - args.organization, - args.repository, - dependabot_file, - args.svg_output, - args.png_output, - args.retrieve_issues, - args.generate_graphs, - args.generate_page, - ) + + if args.all_actions is True: + organization = "lightspeed-core" + repositories = [ + "lightspeed-stack", + "lightspeed-providers", + "rag-content", + ] + for repository in repositories: + dependabot_file = f"{organization}__{repository}.json" + process_issues_for_repository( + organization, + repository, + dependabot_file, + args.svg_output, + args.png_output, + True, + True, + True, + ) + else: + dependabot_file = dependabot_file_name(args) + + return process_issues_for_repository( + args.organization, + args.repository, + dependabot_file, + args.svg_output, + args.png_output, + args.retrieve_issues, + args.generate_graphs, + args.generate_page, + ) if __name__ == "__main__": From 5e7af4a840ae6e41e14ab4eac91ff63f0dc3d326 Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 2 Nov 2026 09:23:04 +0100 Subject: [PATCH 4/5] Better checks --- scripts/vulnerability_report.py | 67 ++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 18 deletions(-) diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 21b4ba64e..668c4e0a9 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -51,6 +51,14 @@ type DependabotAlert = dict[str, Any] type DependabotAlerts = list[DependabotAlert] +DEFAULT_ORGANIZATION = "lightspeed-core" + +LCORE_REPOSITORIES = [ + "lightspeed-stack", + "lightspeed-providers", + "rag-content", +] + def create_argument_parser() -> ArgumentParser: """Create and configure argument parser object. @@ -77,14 +85,14 @@ def create_argument_parser() -> ArgumentParser: "--organization", default="", help="GitHub organization.", - required=True, + required=False, ) cli_parser.add_argument( "--repository", default="", help="GitHub repository.", - required=True, + required=False, ) cli_parser.add_argument( @@ -160,11 +168,18 @@ def check_args(args: Namespace) -> None: ValueError: If graph generation is requested but neither SVG nor PNG output is selected. """ - if args.generate_graphs: + if args.all_actions or 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: msg = "Graphs generation was requested: you need to select the PNG and/or SVG output." raise ValueError(msg) + if not args.all_actions: + if args.organization == "": + msg = "Organization name needs to be specified" + raise ValueError(msg) + if args.repository == "": + msg = "Repository name needs to be specified" + raise ValueError(msg) def dependabot_file_name(args: Namespace) -> str: @@ -588,7 +603,6 @@ def generate_new_cve_dates_graph( 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. @@ -602,9 +616,14 @@ def generate_new_cve_dates_graph( dates, counts = zip(*sorted(data.items(), key=lambda x: x[0])) ax.plot(dates, counts, label="CVEs per day") ax.set_title("New CVEs timeline") - s = pd.Series(counts, index=pd.to_datetime(list(dates))) - ma = s.rolling(window=7, min_periods=1, center=True).mean() - ax.plot(ma.index, ma.values, label="7-day moving average", linewidth=2) + serie = pd.Series(counts, index=pd.to_datetime(list(dates))) + moving_average = serie.rolling(window=7, min_periods=1, center=True).mean() + ax.plot( + moving_average.index, + moving_average.values, + label="7-day moving average", + linewidth=2, + ) # horizontal line with "total" average avg = s.mean() @@ -768,7 +787,6 @@ def retrieve_issues(organization: str, repository: str, dependabot_file: str) -> print(f" repository: {repository}") print(f" output file: {dependabot_file}") - #url = f"https://api.github.com/repos/{organization}/{repository}/dependabot/alerts?state=open" url = f"https://api.github.com/repos/{organization}/{repository}/dependabot/alerts" headers = { @@ -826,13 +844,29 @@ def process_issues_for_repository( ) -> int: """Retrieve alerts or process the local file and generate selected outputs. + Parameters: + organization (str): name of GitHub organization + repository (str): name of GitHub repository + dependabot_file (str): file to store Dependabot issues + svg_output (bool): if set to true, CVE statitic is exported into SVG graphs + png_output (bool): if set to true, CVE statitic is exported into PNG graphs + retrieve_issues_operation (bool): enable retrieving Dependabot issues + generate_graphs_operation (bool): enable generating graphs in SVG or PNG format + generate_page_operation (bool): enable generating page with overall status + Retrieval takes precedence over report generation and returns its status. Otherwise return 0 after processing; file, data, and plotting errors propagate. """ prefix = repository + result = 0 if retrieve_issues_operation: # retrieve issues - retrieve_issues(organization, repository, dependabot_file) + result += retrieve_issues(organization, repository, dependabot_file) + + if result > 0: + print("Error retrieving issues") + print("Skipping graphs and pages generation") + return 1 stat = process_dependabot_file(dependabot_file) @@ -877,16 +911,12 @@ def main() -> int: args = cli_parser.parse_args() check_args(args) - if args.all_actions is True: - organization = "lightspeed-core" - repositories = [ - "lightspeed-stack", - "lightspeed-providers", - "rag-content", - ] - for repository in repositories: + if args.all_actions: + errors = 0 + for repository in LCORE_REPOSITORIES: + organization = DEFAULT_ORGANIZATION dependabot_file = f"{organization}__{repository}.json" - process_issues_for_repository( + errors += process_issues_for_repository( organization, repository, dependabot_file, @@ -896,6 +926,7 @@ def main() -> int: True, True, ) + return 1 if errors > 0 else 0 else: dependabot_file = dependabot_file_name(args) From 925ea6c59f8099608eebd9c3c7601ee835b76e3e Mon Sep 17 00:00:00 2001 From: Pavel Tisnovsky Date: Mon, 2 Nov 2026 11:36:57 +0100 Subject: [PATCH 5/5] New devel dependency --- pyproject.toml | 1 + scripts/vulnerability_report.py | 50 +++++++++++++++++---------------- uv.lock | 2 ++ 3 files changed, 29 insertions(+), 24 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01a53ea07..efc9ca9bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,6 +164,7 @@ dev = [ "pygal>=3.1.3", "pygaljs>=1.0.2", "pdm>=2.29.2", + "pandas>=2.2.3", ] ogxlibdev = [ # To check OGX API provider dependecies: diff --git a/scripts/vulnerability_report.py b/scripts/vulnerability_report.py index 668c4e0a9..4cc0d35db 100644 --- a/scripts/vulnerability_report.py +++ b/scripts/vulnerability_report.py @@ -613,11 +613,15 @@ def generate_new_cve_dates_graph( _, ax = plt.subplots(figsize=(14, 7)) data = stat["dates"] + if not data: + save_graph(prefix, "timeline", svg_output, png_output) + return + dates, counts = zip(*sorted(data.items(), key=lambda x: x[0])) ax.plot(dates, counts, label="CVEs per day") ax.set_title("New CVEs timeline") - serie = pd.Series(counts, index=pd.to_datetime(list(dates))) - moving_average = serie.rolling(window=7, min_periods=1, center=True).mean() + cve_series = pd.Series(counts, index=pd.to_datetime(list(dates))) + moving_average = cve_series.rolling(window=7, min_periods=1, center=True).mean() ax.plot( moving_average.index, moving_average.values, @@ -626,7 +630,7 @@ def generate_new_cve_dates_graph( ) # horizontal line with "total" average - avg = s.mean() + avg = cve_series.mean() ax.axhline( avg, color="red", @@ -838,9 +842,9 @@ def process_issues_for_repository( dependabot_file: str, svg_output: bool, png_output: bool, - retrieve_issues_operation: bool, - generate_graphs_operation: bool, - generate_page_operation: bool, + retrieve_issues_operation: bool = True, + generate_graphs_operation: bool = True, + generate_page_operation: bool = True, ) -> int: """Retrieve alerts or process the local file and generate selected outputs. @@ -848,8 +852,8 @@ def process_issues_for_repository( organization (str): name of GitHub organization repository (str): name of GitHub repository dependabot_file (str): file to store Dependabot issues - svg_output (bool): if set to true, CVE statitic is exported into SVG graphs - png_output (bool): if set to true, CVE statitic is exported into PNG graphs + svg_output (bool): if set to true, CVE statistic is exported into SVG graphs + png_output (bool): if set to true, CVE statistic is exported into PNG graphs retrieve_issues_operation (bool): enable retrieving Dependabot issues generate_graphs_operation (bool): enable generating graphs in SVG or PNG format generate_page_operation (bool): enable generating page with overall status @@ -922,24 +926,22 @@ def main() -> int: dependabot_file, args.svg_output, args.png_output, - True, - True, - True, ) return 1 if errors > 0 else 0 - else: - dependabot_file = dependabot_file_name(args) - - return process_issues_for_repository( - args.organization, - args.repository, - dependabot_file, - args.svg_output, - args.png_output, - args.retrieve_issues, - args.generate_graphs, - args.generate_page, - ) + + # processing for one selected repository + dependabot_file = dependabot_file_name(args) + + return process_issues_for_repository( + args.organization, + args.repository, + dependabot_file, + args.svg_output, + args.png_output, + args.retrieve_issues, + args.generate_graphs, + args.generate_page, + ) if __name__ == "__main__": diff --git a/uv.lock b/uv.lock index 4573cff11..e9594cb8b 100644 --- a/uv.lock +++ b/uv.lock @@ -2465,6 +2465,7 @@ dev = [ { name = "memray" }, { name = "mypy" }, { name = "openapi-to-md" }, + { name = "pandas" }, { name = "pdm" }, { name = "pip" }, { name = "pybuild-deps" }, @@ -2588,6 +2589,7 @@ dev = [ { name = "memray", specifier = ">=1.13.0" }, { name = "mypy", specifier = ">=2.0.0" }, { name = "openapi-to-md", specifier = ">=0.1.0b2" }, + { name = "pandas", specifier = ">=2.2.3" }, { name = "pdm", specifier = ">=2.29.2" }, { name = "pip", specifier = "==26.1" }, { name = "pybuild-deps", specifier = ">=0.5.0" },