From 74707d8ae915760c6a30169fc285033a1d4d781e Mon Sep 17 00:00:00 2001 From: nicooxxx Date: Tue, 14 Jul 2026 13:50:45 +0200 Subject: [PATCH 1/4] Add Dependabot alerts export script --- .gitignore | 6 +- scripts/export_dependabot_alerts.py | 136 ++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 1 deletion(-) create mode 100644 scripts/export_dependabot_alerts.py diff --git a/.gitignore b/.gitignore index 95afcd34..521adcca 100644 --- a/.gitignore +++ b/.gitignore @@ -53,4 +53,8 @@ VERCEL_MIGRATION_GUIDE.md node_modules Agents.md report-analyst.code-workspace -.vscode/launch.json \ No newline at end of file +.vscode/launch.json + +# Dependabot +depenabot_alerts.csv +/depenabot_alerts \ No newline at end of file diff --git a/scripts/export_dependabot_alerts.py b/scripts/export_dependabot_alerts.py new file mode 100644 index 00000000..cb139483 --- /dev/null +++ b/scripts/export_dependabot_alerts.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 + +import csv +import json +import logging +import subprocess +import sys +from pathlib import Path +logger = logging.getLogger(__name__) + + +def run_gh_api(owner: str, repo: str) -> list[dict]: + """Retrieve all Dependabot alerts for a GitHub repository.""" + + # Build the GitHub CLI command for the Dependabot Alerts API + # --paginate requests all result pages instead of only the first page + command = [ + "gh", + "api", + f"repos/{owner}/{repo}/dependabot/alerts", + "--paginate", + ] + + # Execute the GitHub CLI command as a subprocess. + # + # check=True: + # Raises an exception if the command exits with an error. + # capture_output=True: + # Captures stdout and stderr instead of printing them to the terminal. + # text=True: + # Returns the captured output as strings instead of bytes. + result = subprocess.run( + command, + check=True, + capture_output=True, + text=True, + ) + + # Convert the JSON response returned by the GitHub API + # into a Python list containing one dictionary per alert. + return json.loads(result.stdout) + + +def main() -> None: + # Read repository information from command-line arguments. + # Fall back to the default repository when no arguments are provided. + owner = sys.argv[1] if len(sys.argv) > 1 else "climateandtech" + print(sys.argv) + repo = sys.argv[2] if len(sys.argv) > 2 else "report-analyst" + + # Use the third command-line argument as the output path. + # If it is missing, save the CSV in the current directory. + output_path = Path(sys.argv[3] if len(sys.argv) > 3 else "../report-analyst/data/dependabot_alerts/dependabot_alerts.csv") + + # Retrieve all Dependabot alerts from the selected repository. + alerts = run_gh_api(owner, repo) + + # Transform the nested GitHub API response into flat dictionaries + # that can be written directly as rows in a CSV file. + rows = [] + + for alert in alerts: + # Extract nested objects from the alert. + # Empty dictionaries prevent errors when optional fields are missing. + advisory = alert.get("security_advisory", {}) + vulnerability = alert.get("security_vulnerability", {}) + dependency = alert.get("dependency", {}) + package = dependency.get("package", {}) + + # first_patched_version can be null when no patched version exists. + # Using `or {}` ensures that `.get()` can still be called safely. + patched = vulnerability.get("first_patched_version") or {} + + # Select and flatten the relevant alert properties for the CSV output. + # Missing values are replaced with empty strings. + rows.append( + { + "number": alert.get("number", ""), + "severity": advisory.get("severity", ""), + "package": package.get("name", ""), + "ecosystem": package.get("ecosystem", ""), + "manifest": dependency.get("manifest_path", ""), + "scope": dependency.get("scope", ""), + "state": alert.get("state", ""), + "ghsa_id": advisory.get("ghsa_id", ""), + "cve_id": advisory.get("cve_id", ""), + "summary": advisory.get("summary", ""), + "vulnerable_range": vulnerability.get( + "vulnerable_version_range", + "", + ), + "patched_version": patched.get("identifier", ""), + "created_at": alert.get("created_at", ""), + "url": alert.get("html_url", ""), + } + ) + + # Define the column names and their order in the generated CSV file. + fieldnames = [ + "number", + "severity", + "package", + "ecosystem", + "manifest", + "scope", + "state", + "ghsa_id", + "cve_id", + "summary", + "vulnerable_range", + "patched_version", + "created_at", + "url", + ] + + # Create or overwrite the CSV file. + # + # newline="" prevents additional blank lines on some operating systems. + # UTF-8 ensures that special characters are written correctly. + with output_path.open("w", newline="", encoding="utf-8") as file: + writer = csv.DictWriter(file, fieldnames=fieldnames) + + # Write the column names as the first CSV row. + writer.writeheader() + + # Write all transformed Dependabot alerts to the CSV file. + writer.writerows(rows) + + # Print a short confirmation including the number of exported alerts. + print(f"Exported {len(rows)} alerts to {output_path}") + + +# Run main() only when this file is executed directly. +# It will not run automatically when the file is imported as a module. +if __name__ == "__main__": + main() \ No newline at end of file From 6fd79b916fd52c7c0d459a76fa1d73debe037e42 Mon Sep 17 00:00:00 2001 From: nicooxxx Date: Wed, 15 Jul 2026 10:52:58 +0200 Subject: [PATCH 2/4] Formatting with black --- scripts/export_dependabot_alerts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/export_dependabot_alerts.py b/scripts/export_dependabot_alerts.py index cb139483..18c891a2 100644 --- a/scripts/export_dependabot_alerts.py +++ b/scripts/export_dependabot_alerts.py @@ -6,6 +6,7 @@ import subprocess import sys from pathlib import Path + logger = logging.getLogger(__name__) @@ -133,4 +134,4 @@ def main() -> None: # Run main() only when this file is executed directly. # It will not run automatically when the file is imported as a module. if __name__ == "__main__": - main() \ No newline at end of file + main() From 36e2d50ba4b5cdd86ae52da0227762249c7011db Mon Sep 17 00:00:00 2001 From: nicooxxx Date: Thu, 16 Jul 2026 14:38:42 +0200 Subject: [PATCH 3/4] Add get_repo_info for automatic repository information --- scripts/export_dependabot_alerts.py | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/scripts/export_dependabot_alerts.py b/scripts/export_dependabot_alerts.py index 18c891a2..9c2fbfdf 100644 --- a/scripts/export_dependabot_alerts.py +++ b/scripts/export_dependabot_alerts.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import csv +import datetime import json import logging import subprocess @@ -10,6 +11,20 @@ logger = logging.getLogger(__name__) +def get_repo_info() -> tuple[str, str]: + """Retrieve the current GitHub owner and repository name using gh CLI.""" + result = subprocess.run( + ["gh", "repo", "view", "--json", "owner,name"], + check=True, + capture_output=True, + text=True, + ) + data = json.loads(result.stdout) + owner = data["owner"]["login"] + repo = data["name"] + return owner, repo + + def run_gh_api(owner: str, repo: str) -> list[dict]: """Retrieve all Dependabot alerts for a GitHub repository.""" @@ -43,17 +58,11 @@ def run_gh_api(owner: str, repo: str) -> list[dict]: def main() -> None: - # Read repository information from command-line arguments. - # Fall back to the default repository when no arguments are provided. - owner = sys.argv[1] if len(sys.argv) > 1 else "climateandtech" - print(sys.argv) - repo = sys.argv[2] if len(sys.argv) > 2 else "report-analyst" - - # Use the third command-line argument as the output path. - # If it is missing, save the CSV in the current directory. - output_path = Path(sys.argv[3] if len(sys.argv) > 3 else "../report-analyst/data/dependabot_alerts/dependabot_alerts.csv") + owner, repo = get_repo_info() + date = datetime.datetime.now().strftime("%Y-%m-%dT%H-%M") + output_path = Path(f"../{repo}/data/dependabot_alerts/{date}_dependabot_alerts.csv") - # Retrieve all Dependabot alerts from the selected repository. + # Retrieve all Dependabot alerts from the selected repository alerts = run_gh_api(owner, repo) # Transform the nested GitHub API response into flat dictionaries From 3ca08f321eae59d74659a064b0016656e722cfbb Mon Sep 17 00:00:00 2001 From: nicooxxx Date: Mon, 20 Jul 2026 12:34:09 +0200 Subject: [PATCH 4/4] Fix Ruff formatting issues --- scripts/export_dependabot_alerts.py | 1 - 1 file changed, 1 deletion(-) diff --git a/scripts/export_dependabot_alerts.py b/scripts/export_dependabot_alerts.py index 9c2fbfdf..c2ef3110 100644 --- a/scripts/export_dependabot_alerts.py +++ b/scripts/export_dependabot_alerts.py @@ -5,7 +5,6 @@ import json import logging import subprocess -import sys from pathlib import Path logger = logging.getLogger(__name__)