Skip to content

Commit 84d4ebb

Browse files
committed
Closing code scanning alerts en masse
1 parent d5af962 commit 84d4ebb

File tree

2 files changed

+151
-5
lines changed

2 files changed

+151
-5
lines changed

close_code_scanning_alerts.py

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
#!/usr/bin/env python3
2+
3+
"""Close all open code scanning alerts for a repository."""
4+
5+
from githubapi import GitHub
6+
import argparse
7+
import logging
8+
from tqdm import tqdm
9+
10+
11+
LOG = logging.getLogger(__name__)
12+
13+
14+
def update_code_scanning_alert(
15+
g: GitHub,
16+
repo_name: str,
17+
alert_number: int,
18+
state: str,
19+
resolution: str,
20+
resolution_comment: str,
21+
dry_run: bool = False,
22+
) -> None:
23+
"""Update a code scanning alert using the GitHub API."""
24+
state_update = {
25+
"state": state,
26+
"dismissed_reason": resolution,
27+
"dismissed_comment": resolution_comment,
28+
"create_request": False,
29+
}
30+
31+
if not dry_run:
32+
g.query_once(
33+
"repo",
34+
repo_name,
35+
f"/code-scanning/alerts/{alert_number}",
36+
data=state_update,
37+
method="PATCH",
38+
)
39+
else:
40+
print(
41+
f"Would have updated alert {repo_name}#{alert_number} with {state_update}"
42+
)
43+
44+
45+
def close_code_scanning_alerts(
46+
github: GitHub, owner: str, repo: str, resolution: str, dry_run: bool = False
47+
) -> None:
48+
"""Close all open code scanning alerts for a repository."""
49+
repo_name = f"{owner}/{repo}"
50+
51+
# Get all open code scanning alerts for the repository.
52+
alerts = github.list_code_scanning_alerts(repo_name, scope="repo", state="open", progress=False)
53+
54+
counter = 0
55+
56+
with tqdm(total=None, desc=f"Closing alerts for {repo_name}", unit=" alerts") as pbar:
57+
# Close each alert.
58+
for alert in alerts:
59+
update_code_scanning_alert(
60+
github,
61+
repo_name,
62+
alert["number"],
63+
state="dismissed",
64+
resolution=resolution,
65+
resolution_comment="Closed by scripting",
66+
dry_run=dry_run,
67+
)
68+
counter += 1
69+
pbar.update(1)
70+
71+
print(f"Closed {counter} code scanning alerts for {owner}/{repo}")
72+
return None
73+
74+
75+
def add_args(parser: argparse.ArgumentParser) -> None:
76+
"""Add command line arguments to the parser."""
77+
parser.add_argument(
78+
"repo_name",
79+
type=str,
80+
help="The owner/repo of the repository to close alerts for.",
81+
)
82+
parser.add_argument(
83+
"--resolution",
84+
type=str,
85+
default="used in tests",
86+
choices=["false positive", "won't fix", "used in tests"],
87+
help="The resolution of the alert.",
88+
)
89+
parser.add_argument(
90+
"--dry-run",
91+
action="store_true",
92+
help="Print the alerts that would be closed, but don't actually close them.",
93+
)
94+
parser.add_argument(
95+
"-d",
96+
"--debug",
97+
action="store_true",
98+
help="Print debug messages to the console.",
99+
)
100+
return None
101+
102+
103+
def main() -> None:
104+
"""Command-line entry point."""
105+
parser = argparse.ArgumentParser(description=__doc__)
106+
add_args(parser)
107+
args = parser.parse_args()
108+
109+
logging.basicConfig(level=logging.INFO if not args.debug else logging.DEBUG)
110+
111+
github = GitHub()
112+
113+
try:
114+
owner, repo = args.repo_name.split("/")
115+
except ValueError:
116+
LOG.error(f"Invalid repository name: {args.repo_name}")
117+
exit(1)
118+
119+
close_code_scanning_alerts(
120+
github, owner, repo, args.resolution, dry_run=args.dry_run
121+
)
122+
123+
return None
124+
125+
126+
if __name__ == "__main__":
127+
main()

githubapi.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def query(
9999
since: datetime.datetime | None = None,
100100
date_field: str = "created_at",
101101
paging: None | str = "cursor",
102+
progress: bool = True,
102103
) -> Generator[dict, None, None]:
103104
"""Query the GitHub API."""
104105
LOG.debug(method)
@@ -118,7 +119,7 @@ def query(
118119
LOG.debug("".join(traceback.format_exception(e)))
119120
else:
120121
for result in self.paginate(
121-
url, since, date_field=date_field, cursor=paging == "cursor"
122+
url, since, date_field=date_field, cursor=paging == "cursor", progress=progress
122123
):
123124
yield result
124125

@@ -290,6 +291,8 @@ def paginate(
290291
pbar = tqdm(desc="GitHub API", unit=" requests")
291292
pbar.reset(total=None)
292293

294+
direction = ""
295+
293296
while True:
294297
try:
295298
try:
@@ -310,7 +313,8 @@ def paginate(
310313
if data is None or response is None:
311314
break
312315

313-
pbar.update(1)
316+
if progress:
317+
pbar.update(1)
314318

315319
LOG.debug(data)
316320

@@ -337,16 +341,29 @@ def paginate(
337341
link_header = response.headers.get("Link")
338342
if not link_header:
339343
LOG.debug("No link header, stopping retrieval")
344+
LOG.debug(response.headers)
340345
break
341346
links = self.parse_link_header(link_header)
342347

343348
LOG.debug(links)
344349

345-
if "next" not in links:
346-
LOG.debug("No next link, stopping retrieval")
350+
if direction == "":
351+
if "next" in links:
352+
direction = "next"
353+
elif "prev" in links:
354+
direction = "prev"
355+
else:
356+
LOG.debug("No next or prev link")
357+
break
358+
359+
if direction == "next" and "next" not in links:
360+
LOG.debug("No more results, stopping retrieval")
361+
break
362+
elif direction == "prev" and "prev" not in links:
363+
LOG.debug("No more results, stopping retrieval")
347364
break
348365

349-
url = links["next"]
366+
url = links[direction]
350367

351368
except KeyboardInterrupt:
352369
LOG.warning("Interrupted by user, stopping with what we have")
@@ -363,6 +380,7 @@ def list_code_scanning_alerts(
363380
state: str | None = None,
364381
since: datetime.datetime | None = None,
365382
scope: str = "org",
383+
progress: bool = True,
366384
) -> Generator[dict, None, None]:
367385
"""List code scanning alerts for a GitHub repository, organization or Enterprise."""
368386
query = {"state": state} if state is not None else {}
@@ -374,6 +392,7 @@ def list_code_scanning_alerts(
374392
since=since,
375393
date_field="created_at",
376394
paging="cursor",
395+
progress=progress,
377396
)
378397

379398
results = (

0 commit comments

Comments
 (0)