Skip to content

Commit 9fc7e09

Browse files
authored
Merge pull request #4 from advanced-security:copilot/add-dependabot-alerts-script
Add Dependabot alerts listing support
2 parents fb427aa + 5d2fbf2 commit 9fc7e09

File tree

3 files changed

+297
-0
lines changed

3 files changed

+297
-0
lines changed

README.md

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,41 @@ options:
104104
--debug, -d Enable debug logging
105105
```
106106

107+
### List Dependabot alerts
108+
109+
This script retrieves Dependabot alerts from GitHub repositories, organizations, or Enterprises and outputs them in CSV or JSON format. It supports filtering by state and date. Use this to audit, track, or export Dependabot security vulnerability findings for dependency management and reporting.
110+
111+
```text
112+
usage: list_dependabot_alerts.py [-h] [--scope {ent,org,repo}] [--state {auto_dismissed,dismissed,fixed,open}]
113+
[--since SINCE] [--json] [--raw] [--quote-all] [--hostname HOSTNAME]
114+
[--ca-cert-bundle CA_CERT_BUNDLE] [--no-verify-tls] [--quiet] [--debug]
115+
name
116+
117+
List Dependabot alerts for a GitHub repository, organization or Enterprise.
118+
119+
positional arguments:
120+
name Name of the repo/org/Enterprise to query
121+
122+
options:
123+
-h, --help show this help message and exit
124+
--scope {ent,org,repo}
125+
Scope of the query
126+
--state {auto_dismissed,dismissed,fixed,open}, -s {auto_dismissed,dismissed,fixed,open}
127+
State of the alerts to query
128+
--since SINCE, -S SINCE
129+
Only show alerts created after this date/time - ISO 8601 format, e.g. 2024-10-08 or
130+
2024-10-08T12:00; or Nd format, e.g. 7d for 7 days ago
131+
--json Output in JSON format (otherwise CSV)
132+
--raw, -r Output raw JSON data from the API
133+
--quote-all, -q Quote all fields in CSV output
134+
--hostname HOSTNAME GitHub Enterprise hostname (defaults to github.com)
135+
--ca-cert-bundle CA_CERT_BUNDLE, -C CA_CERT_BUNDLE
136+
Path to CA certificate bundle in PEM format (e.g. for self-signed server certificates)
137+
--no-verify-tls Do not verify TLS connection certificates (warning: insecure)
138+
--quiet Suppress non-error log messages
139+
--debug, -d Enable debug logging
140+
```
141+
107142
### Replay code scanning alert status
108143

109144
This script replays or restores the status of code scanning alerts based on a previously exported CSV file. It's useful when alerts need to be re-dismissed after a repository is recreated or when migrating alert states between environments. The script reads from stdin and matches alerts by location.

githubapi.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,39 @@ def list_secret_scanning_alerts(
462462

463463
return results
464464

465+
def list_dependabot_alerts(
466+
self,
467+
name: str,
468+
state: str | None = None,
469+
since: datetime.datetime | None = None,
470+
scope: str = "org",
471+
progress: bool = True,
472+
) -> Generator[dict, None, None]:
473+
"""List Dependabot alerts for a GitHub repository, organization or Enterprise."""
474+
query = {"state": state} if state is not None else {}
475+
476+
alerts = self.query(
477+
scope,
478+
name,
479+
"/dependabot/alerts",
480+
query,
481+
since=since,
482+
date_field="created_at",
483+
paging="cursor",
484+
progress=progress,
485+
)
486+
487+
results = (
488+
alert
489+
for alert in alerts
490+
if (
491+
since is None
492+
or datetime.datetime.fromisoformat(alert["created_at"]) >= since
493+
)
494+
)
495+
496+
return results
497+
465498

466499
def parse_date(date: str) -> datetime.datetime | None:
467500
"""Parse a date string and return a datetime object.

list_dependabot_alerts.py

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
#!/usr/bin/env python3
2+
3+
"""List Dependabot alerts for a GitHub repository, organization or Enterprise."""
4+
5+
import sys
6+
import argparse
7+
import logging
8+
import datetime
9+
import json
10+
from typing import Generator
11+
from defusedcsv import csv # type: ignore
12+
from githubapi import GitHub, parse_date
13+
14+
15+
LOG = logging.getLogger(__name__)
16+
17+
18+
def make_result(
19+
alert: dict, scope: str, name: str
20+
) -> dict:
21+
"""Make an alert result from the raw data."""
22+
result = {
23+
"created_at": alert["created_at"],
24+
"repo": alert["repository"]["full_name"] if scope != "repo" and "repository" in alert else name,
25+
"url": alert["html_url"],
26+
"state": alert["state"],
27+
"dismissed_at": alert["dismissed_at"],
28+
"dismissed_by": alert["dismissed_by"]["login"] if alert["dismissed_by"] else None,
29+
"dismissed_reason": alert["dismissed_reason"],
30+
"dismissed_comment": alert["dismissed_comment"],
31+
"fixed_at": alert["fixed_at"],
32+
"auto_dismissed_at": alert.get("auto_dismissed_at"),
33+
"package_name": alert["security_advisory"]["package"]["name"],
34+
"package_ecosystem": alert["security_advisory"]["package"]["ecosystem"],
35+
"severity": alert["security_advisory"]["severity"],
36+
"cve_id": alert["security_advisory"]["cve_id"],
37+
"ghsa_id": alert["security_advisory"]["ghsa_id"],
38+
"summary": alert["security_advisory"]["summary"],
39+
"description": alert["security_advisory"]["description"],
40+
"vulnerable_version_range": alert["security_vulnerability"]["vulnerable_version_range"],
41+
"first_patched_version": alert["security_vulnerability"]["first_patched_version"]["identifier"] if alert["security_vulnerability"]["first_patched_version"] else None,
42+
"manifest_path": alert["dependency"]["manifest_path"] if "dependency" in alert and alert["dependency"] else None,
43+
"scope": alert["dependency"]["scope"] if "dependency" in alert and alert["dependency"] else None,
44+
}
45+
46+
return result
47+
48+
49+
def to_list(result: dict) -> list[str|None]:
50+
return [
51+
result["created_at"],
52+
result["repo"],
53+
result["url"],
54+
result["state"],
55+
result["dismissed_at"],
56+
result["dismissed_by"],
57+
result["dismissed_reason"],
58+
result["dismissed_comment"],
59+
result["fixed_at"],
60+
result["auto_dismissed_at"],
61+
result["package_name"],
62+
result["package_ecosystem"],
63+
result["severity"],
64+
result["cve_id"],
65+
result["ghsa_id"],
66+
result["summary"],
67+
result["description"],
68+
result["vulnerable_version_range"],
69+
result["first_patched_version"],
70+
result["manifest_path"],
71+
result["scope"],
72+
]
73+
74+
75+
def output_csv(results: list[dict], quote_all: bool) -> None:
76+
"""Write the results to stdout as CSV."""
77+
writer = csv.writer(
78+
sys.stdout, quoting=csv.QUOTE_ALL if quote_all else csv.QUOTE_MINIMAL
79+
)
80+
81+
writer.writerow(
82+
[
83+
"created_at",
84+
"repo",
85+
"url",
86+
"state",
87+
"dismissed_at",
88+
"dismissed_by",
89+
"dismissed_reason",
90+
"dismissed_comment",
91+
"fixed_at",
92+
"auto_dismissed_at",
93+
"package_name",
94+
"package_ecosystem",
95+
"severity",
96+
"cve_id",
97+
"ghsa_id",
98+
"summary",
99+
"description",
100+
"vulnerable_version_range",
101+
"first_patched_version",
102+
"manifest_path",
103+
"scope",
104+
]
105+
)
106+
107+
for result in results:
108+
writer.writerow(to_list(result))
109+
110+
111+
def list_dependabot_alerts(name: str, scope: str, hostname: str, state: str|None=None, since: datetime.datetime|None=None, raw: bool=False, verify: bool | str = True, progress: bool = True) -> Generator[dict, None, None]:
112+
g = GitHub(hostname=hostname, verify=verify)
113+
alerts = g.list_dependabot_alerts(name, state=state, since=since, scope=scope, progress=progress)
114+
if raw:
115+
return alerts
116+
else:
117+
results = (make_result(alert, scope, name) for alert in alerts)
118+
return results
119+
120+
121+
def add_args(parser: argparse.ArgumentParser) -> None:
122+
"""Add command-line arguments to the parser."""
123+
parser.add_argument(
124+
"name", type=str, help="Name of the repo/org/Enterprise to query"
125+
)
126+
parser.add_argument(
127+
"--scope",
128+
type=str,
129+
default="org",
130+
choices=["ent", "org", "repo"],
131+
required=False,
132+
help="Scope of the query",
133+
)
134+
parser.add_argument(
135+
"--state",
136+
"-s",
137+
type=str,
138+
choices=["auto_dismissed", "dismissed", "fixed", "open"],
139+
required=False,
140+
help="State of the alerts to query",
141+
)
142+
parser.add_argument(
143+
"--since",
144+
"-S",
145+
type=str,
146+
required=False,
147+
help="Only show alerts created after this date/time - ISO 8601 format, e.g. 2024-10-08 or 2024-10-08T12:00; or Nd format, e.g. 7d for 7 days ago",
148+
)
149+
parser.add_argument(
150+
"--json", action="store_true", help="Output in JSON format (otherwise CSV)"
151+
)
152+
parser.add_argument(
153+
"--raw", "-r", action="store_true", help="Output raw JSON data from the API"
154+
)
155+
parser.add_argument(
156+
"--quote-all", "-q", action="store_true", help="Quote all fields in CSV output"
157+
)
158+
parser.add_argument(
159+
"--hostname",
160+
type=str,
161+
default="github.com",
162+
required=False,
163+
help="GitHub Enterprise hostname (defaults to github.com)",
164+
)
165+
parser.add_argument(
166+
"--ca-cert-bundle",
167+
"-C",
168+
type=str,
169+
required=False,
170+
help="Path to CA certificate bundle in PEM format (e.g. for self-signed server certificates)"
171+
)
172+
parser.add_argument(
173+
"--no-verify-tls",
174+
action="store_true",
175+
help="Do not verify TLS connection certificates (warning: insecure)"
176+
)
177+
parser.add_argument(
178+
"--quiet",
179+
action="store_true",
180+
help="Suppress non-error log messages",
181+
)
182+
parser.add_argument(
183+
"--debug", "-d", action="store_true", help="Enable debug logging"
184+
)
185+
186+
187+
def main() -> None:
188+
"""CLI entrypoint."""
189+
parser = argparse.ArgumentParser(description=__doc__)
190+
add_args(parser)
191+
args = parser.parse_args()
192+
193+
logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO if not args.quiet else logging.ERROR, format="%(asctime)s %(levelname)s %(message)s")
194+
195+
since = parse_date(args.since)
196+
197+
LOG.debug("Since: %s (%s) [%s]", since, args.since, type(since))
198+
199+
if args.raw:
200+
args.json = True
201+
202+
scope = "repo" if ("/" in args.name and args.scope != "repo") else args.scope
203+
name = args.name
204+
state = args.state
205+
hostname = args.hostname
206+
verify = True
207+
208+
if args.ca_cert_bundle:
209+
verify = args.ca_cert_bundle
210+
211+
if args.no_verify_tls:
212+
verify = False
213+
LOG.warning("Disabling TLS verification. This is insecure and should not be used in production")
214+
import urllib3
215+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
216+
217+
if not GitHub.check_name(name, scope):
218+
raise ValueError("Invalid name: %s for %s", name, scope)
219+
220+
results = list_dependabot_alerts(name, scope, hostname, state=state, since=since, raw=args.raw, verify=verify, progress=not args.quiet)
221+
222+
if args.json:
223+
print(json.dumps(list(results), indent=2))
224+
else:
225+
output_csv(results, args.quote_all) # type: ignore
226+
227+
228+
if __name__ == "__main__":
229+
main()

0 commit comments

Comments
 (0)