Skip to content

Commit f69f69c

Browse files
authored
Enhance secret scanning API calls to return more data (#87)
* Enhance secret scanning API calls to return more data * run black * black with superlinter * Add Black configuration for code formatting to pyproject.toml so it can be used local with black (and picked up by superlinter) * Fix super-linter configuration to ensure Black uses the correct pyproject.toml file * Add Black configuration to super-linter and ensure proper validation
1 parent 2b801a8 commit f69f69c

File tree

2 files changed

+185
-27
lines changed

2 files changed

+185
-27
lines changed

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[tool.black]
2+
line-length = 120

src/secret_scanning.py

Lines changed: 183 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,12 @@ def get_repo_ss_alerts(api_endpoint, github_pat, repo_name):
1818
- List of _all_ secret scanning alerts on the repository (both default and generic secret types)
1919
"""
2020
# First call: get default secret types (without any filters), use after= to force object based cursor instead of page based
21-
url_default = f"{api_endpoint}/repos/{repo_name}/secret-scanning/alerts?per_page=100&after="
21+
url_default = f"{api_endpoint}/repos/{repo_name}/secret-scanning/alerts?per_page=100&after=&hide_secret=true"
2222
ss_alerts_default = api_helpers.make_api_call(url_default, github_pat)
2323

2424
# Second call: get generic secret types with hardcoded list, use after= to force object based cursor instead of page based
2525
generic_secret_types = "password,http_basic_authentication_header,http_bearer_authentication_header,mongodb_connection_string,mysql_connection_string,openssh_private_key,pgp_private_key,postgres_connection_string,rsa_private_key"
26-
url_generic = f"{api_endpoint}/repos/{repo_name}/secret-scanning/alerts?per_page=100&after=&secret_type={generic_secret_types}"
26+
url_generic = f"{api_endpoint}/repos/{repo_name}/secret-scanning/alerts?per_page=100&after=&secret_type={generic_secret_types}&hide_secret=true"
2727
ss_alerts_generic = api_helpers.make_api_call(url_generic, github_pat)
2828

2929
# Combine results and deduplicate
@@ -73,31 +73,79 @@ def write_repo_ss_list(secrets_list):
7373
[
7474
"number",
7575
"created_at",
76+
"updated_at",
7677
"html_url",
7778
"state",
7879
"resolution",
7980
"resolved_at",
8081
"resolved_by_username",
8182
"resolved_by_type",
8283
"resolved_by_isadmin",
84+
"resolution_comment",
8385
"secret_type",
8486
"secret_type_display_name",
87+
"validity",
88+
"publicly_leaked",
89+
"multi_repo",
90+
"is_base64_encoded",
91+
"first_location_path",
92+
"first_location_start_line",
93+
"first_location_commit_sha",
94+
"push_protection_bypassed",
95+
"push_protection_bypassed_by",
96+
"push_protection_bypassed_at",
97+
"push_protection_bypass_request_reviewer",
98+
"push_protection_bypass_request_reviewer_comment",
99+
"push_protection_bypass_request_comment",
100+
"push_protection_bypass_request_html_url",
101+
"assigned_to",
85102
]
86103
)
87104
for alert in secrets_list:
105+
first_location = alert.get("first_location_detected") or {}
106+
88107
writer.writerow(
89108
[
90109
alert["number"],
91110
alert["created_at"],
111+
alert["updated_at"],
92112
alert["html_url"],
93113
alert["state"],
94114
alert["resolution"],
95115
alert["resolved_at"],
96-
"" if alert["resolved_by"] is None else alert["resolved_by"]["login"],
97-
"" if alert["resolved_by"] is None else alert["resolved_by"]["type"],
98-
"" if alert["resolved_by"] is None else alert["resolved_by"]["site_admin"],
116+
("" if alert["resolved_by"] is None else alert["resolved_by"]["login"]),
117+
("" if alert["resolved_by"] is None else alert["resolved_by"]["type"]),
118+
("" if alert["resolved_by"] is None else alert["resolved_by"]["site_admin"]),
119+
alert.get("resolution_comment", ""),
99120
alert["secret_type"],
100121
alert["secret_type_display_name"],
122+
alert["validity"],
123+
str(alert["publicly_leaked"]),
124+
str(alert["multi_repo"]),
125+
str(alert["is_base64_encoded"]),
126+
first_location.get("path")
127+
or first_location.get("pull_request_body_url")
128+
or first_location.get("issue_body_url")
129+
or first_location.get("discussion_body_url")
130+
or "",
131+
("" if first_location is None else first_location.get("start_line", "")),
132+
("" if first_location is None else first_location.get("commit_sha", "")),
133+
str(alert["push_protection_bypassed"]),
134+
(
135+
""
136+
if alert.get("push_protection_bypassed_by") is None
137+
else alert["push_protection_bypassed_by"].get("login", "")
138+
),
139+
alert.get("push_protection_bypassed_at", ""),
140+
(
141+
""
142+
if alert.get("push_protection_bypass_request_reviewer") is None
143+
else alert["push_protection_bypass_request_reviewer"].get("login", "")
144+
),
145+
alert.get("push_protection_bypass_request_reviewer_comment", ""),
146+
alert.get("push_protection_bypass_request_comment", ""),
147+
alert.get("push_protection_bypass_request_html_url", ""),
148+
("" if alert.get("assigned_to") is None else alert["assigned_to"].get("login", "")),
101149
]
102150
)
103151

@@ -115,32 +163,32 @@ def get_org_ss_alerts(api_endpoint, github_pat, org_name):
115163
- List of _all_ secret scanning alerts on the organization (both default and generic secret types)
116164
"""
117165
# First call: get default secret types (without any filters), use after= to force object based cursor instead of page based
118-
url_default = f"{api_endpoint}/orgs/{org_name}/secret-scanning/alerts?per_page=100&after="
166+
url_default = f"{api_endpoint}/orgs/{org_name}/secret-scanning/alerts?per_page=100&after=&hide_secret=true"
119167
ss_alerts_default = api_helpers.make_api_call(url_default, github_pat)
120168

121169
# Second call: get generic secret types with hardcoded list, use after= to force object based cursor instead of page based
122170
generic_secret_types = "password,http_basic_authentication_header,http_bearer_authentication_header,mongodb_connection_string,mysql_connection_string,openssh_private_key,pgp_private_key,postgres_connection_string,rsa_private_key"
123-
url_generic = (
124-
f"{api_endpoint}/orgs/{org_name}/secret-scanning/alerts?per_page=100&after=&secret_type={generic_secret_types}"
125-
)
171+
url_generic = f"{api_endpoint}/orgs/{org_name}/secret-scanning/alerts?per_page=100&after=&secret_type={generic_secret_types}&hide_secret=true"
126172
ss_alerts_generic = api_helpers.make_api_call(url_generic, github_pat)
127173

128-
# Combine results and deduplicate
174+
# Combine results and deduplicate using composite key (repo + alert number)
129175
combined_alerts = []
130-
alert_numbers_seen = set()
176+
alert_keys_seen = set() # Composite key: (repo, alert_number)
131177
duplicates_found = False
132178

133179
# Add default alerts
134180
for alert in ss_alerts_default:
135-
alert_numbers_seen.add(alert["number"])
181+
alert_key = (alert["repository"]["full_name"], alert["number"])
182+
alert_keys_seen.add(alert_key)
136183
combined_alerts.append(alert)
137184

138185
# Add generic alerts, checking for duplicates
139186
for alert in ss_alerts_generic:
140-
if alert["number"] in alert_numbers_seen:
187+
alert_key = (alert["repository"]["full_name"], alert["number"])
188+
if alert_key in alert_keys_seen:
141189
duplicates_found = True
142190
else:
143-
alert_numbers_seen.add(alert["number"])
191+
alert_keys_seen.add(alert_key)
144192
combined_alerts.append(alert)
145193

146194
# Warn if duplicates were found
@@ -172,15 +220,32 @@ def write_org_ss_list(secrets_list):
172220
[
173221
"number",
174222
"created_at",
223+
"updated_at",
175224
"html_url",
176225
"state",
177226
"resolution",
178227
"resolved_at",
179228
"resolved_by_username",
180229
"resolved_by_type",
181230
"resolved_by_isadmin",
231+
"resolution_comment",
182232
"secret_type",
183233
"secret_type_display_name",
234+
"validity",
235+
"publicly_leaked",
236+
"multi_repo",
237+
"is_base64_encoded",
238+
"first_location_path",
239+
"first_location_start_line",
240+
"first_location_commit_sha",
241+
"push_protection_bypassed",
242+
"push_protection_bypassed_by",
243+
"push_protection_bypassed_at",
244+
"push_protection_bypass_request_reviewer",
245+
"push_protection_bypass_request_reviewer_comment",
246+
"push_protection_bypass_request_comment",
247+
"push_protection_bypass_request_html_url",
248+
"assigned_to",
184249
"repo_name",
185250
"repo_owner",
186251
"repo_owner_type",
@@ -191,19 +256,50 @@ def write_org_ss_list(secrets_list):
191256
]
192257
)
193258
for alert in secrets_list:
259+
first_location = alert.get("first_location_detected") or {}
260+
194261
writer.writerow(
195262
[
196263
alert["number"],
197264
alert["created_at"],
265+
alert["updated_at"],
198266
alert["html_url"],
199267
alert["state"],
200268
alert["resolution"],
201269
alert["resolved_at"],
202-
"" if alert["resolved_by"] is None else alert["resolved_by"]["login"],
203-
"" if alert["resolved_by"] is None else alert["resolved_by"]["type"],
204-
"" if alert["resolved_by"] is None else alert["resolved_by"]["site_admin"],
270+
("" if alert["resolved_by"] is None else alert["resolved_by"]["login"]),
271+
("" if alert["resolved_by"] is None else alert["resolved_by"]["type"]),
272+
("" if alert["resolved_by"] is None else alert["resolved_by"]["site_admin"]),
273+
alert.get("resolution_comment", ""),
205274
alert["secret_type"],
206275
alert["secret_type_display_name"],
276+
alert["validity"],
277+
str(alert["publicly_leaked"]),
278+
str(alert["multi_repo"]),
279+
str(alert["is_base64_encoded"]),
280+
first_location.get("path")
281+
or first_location.get("pull_request_body_url")
282+
or first_location.get("issue_body_url")
283+
or first_location.get("discussion_body_url")
284+
or "",
285+
("" if first_location is None else first_location.get("start_line", "")),
286+
("" if first_location is None else first_location.get("commit_sha", "")),
287+
str(alert["push_protection_bypassed"]),
288+
(
289+
""
290+
if alert.get("push_protection_bypassed_by") is None
291+
else alert["push_protection_bypassed_by"].get("login", "")
292+
),
293+
alert.get("push_protection_bypassed_at", ""),
294+
(
295+
""
296+
if alert.get("push_protection_bypass_request_reviewer") is None
297+
else alert["push_protection_bypass_request_reviewer"].get("login", "")
298+
),
299+
alert.get("push_protection_bypass_request_reviewer_comment", ""),
300+
alert.get("push_protection_bypass_request_comment", ""),
301+
alert.get("push_protection_bypass_request_html_url", ""),
302+
("" if alert.get("assigned_to") is None else alert["assigned_to"].get("login", "")),
207303
alert["repository"]["full_name"],
208304
alert["repository"]["owner"]["login"],
209305
alert["repository"]["owner"]["type"],
@@ -229,30 +325,42 @@ def get_enterprise_ss_alerts(api_endpoint, github_pat, enterprise_slug):
229325
- List of _all_ secret scanning alerts on the enterprise (both default and generic secret types)
230326
"""
231327
# First call: get default secret types (without any filters), use after= to force object based cursor instead of page based
232-
url_default = f"{api_endpoint}/enterprises/{enterprise_slug}/secret-scanning/alerts?per_page=100&after="
328+
url_default = (
329+
f"{api_endpoint}/enterprises/{enterprise_slug}/secret-scanning/alerts?per_page=100&after=&hide_secret=true"
330+
)
233331
ss_alerts_default = api_helpers.make_api_call(url_default, github_pat)
234332

235333
# Second call: get generic secret types with hardcoded list, use after= to force object based cursor instead of page based
236334
generic_secret_types = "password,http_basic_authentication_header,http_bearer_authentication_header,mongodb_connection_string,mysql_connection_string,openssh_private_key,pgp_private_key,postgres_connection_string,rsa_private_key"
237-
url_generic = f"{api_endpoint}/enterprises/{enterprise_slug}/secret-scanning/alerts?per_page=100&after=&secret_type={generic_secret_types}"
335+
url_generic = f"{api_endpoint}/enterprises/{enterprise_slug}/secret-scanning/alerts?per_page=100&after=&secret_type={generic_secret_types}&hide_secret=true"
238336
ss_alerts_generic = api_helpers.make_api_call(url_generic, github_pat)
239337

240-
# Combine results and deduplicate
338+
# Combine results and deduplicate using composite key (org + repo + alert number)
241339
combined_alerts = []
242-
alert_numbers_seen = set()
340+
alert_keys_seen = set() # Composite key: (org, repo, alert_number)
243341
duplicates_found = False
244342

245343
# Add default alerts
246344
for alert in ss_alerts_default:
247-
alert_numbers_seen.add(alert["number"])
345+
alert_key = (
346+
alert["repository"]["owner"]["login"],
347+
alert["repository"]["name"],
348+
alert["number"],
349+
)
350+
alert_keys_seen.add(alert_key)
248351
combined_alerts.append(alert)
249352

250353
# Add generic alerts, checking for duplicates
251354
for alert in ss_alerts_generic:
252-
if alert["number"] in alert_numbers_seen:
355+
alert_key = (
356+
alert["repository"]["owner"]["login"],
357+
alert["repository"]["name"],
358+
alert["number"],
359+
)
360+
if alert_key in alert_keys_seen:
253361
duplicates_found = True
254362
else:
255-
alert_numbers_seen.add(alert["number"])
363+
alert_keys_seen.add(alert_key)
256364
combined_alerts.append(alert)
257365

258366
# Warn if duplicates were found
@@ -284,15 +392,32 @@ def write_enterprise_ss_list(secrets_list):
284392
[
285393
"number",
286394
"created_at",
395+
"updated_at",
287396
"html_url",
288397
"state",
289398
"resolution",
290399
"resolved_at",
291400
"resolved_by_username",
292401
"resolved_by_type",
293402
"resolved_by_isadmin",
403+
"resolution_comment",
294404
"secret_type",
295405
"secret_type_display_name",
406+
"validity",
407+
"publicly_leaked",
408+
"multi_repo",
409+
"is_base64_encoded",
410+
"first_location_path",
411+
"first_location_start_line",
412+
"first_location_commit_sha",
413+
"push_protection_bypassed",
414+
"push_protection_bypassed_by",
415+
"push_protection_bypassed_at",
416+
"push_protection_bypass_request_reviewer",
417+
"push_protection_bypass_request_reviewer_comment",
418+
"push_protection_bypass_request_comment",
419+
"push_protection_bypass_request_html_url",
420+
"assigned_to",
296421
"repo_name",
297422
"repo_owner",
298423
"repo_owner_type",
@@ -303,19 +428,50 @@ def write_enterprise_ss_list(secrets_list):
303428
]
304429
)
305430
for alert in secrets_list:
431+
first_location = alert.get("first_location_detected") or {}
432+
306433
writer.writerow(
307434
[
308435
alert["number"],
309436
alert["created_at"],
437+
alert["updated_at"],
310438
alert["html_url"],
311439
alert["state"],
312440
alert["resolution"],
313441
alert["resolved_at"],
314-
"" if alert["resolved_by"] is None else alert["resolved_by"]["login"],
315-
"" if alert["resolved_by"] is None else alert["resolved_by"]["type"],
316-
"" if alert["resolved_by"] is None else alert["resolved_by"]["site_admin"],
442+
("" if alert["resolved_by"] is None else alert["resolved_by"]["login"]),
443+
("" if alert["resolved_by"] is None else alert["resolved_by"]["type"]),
444+
("" if alert["resolved_by"] is None else alert["resolved_by"]["site_admin"]),
445+
alert.get("resolution_comment", ""),
317446
alert["secret_type"],
318447
alert["secret_type_display_name"],
448+
alert["validity"],
449+
str(alert["publicly_leaked"]),
450+
str(alert["multi_repo"]),
451+
str(alert["is_base64_encoded"]),
452+
first_location.get("path")
453+
or first_location.get("pull_request_body_url")
454+
or first_location.get("issue_body_url")
455+
or first_location.get("discussion_body_url")
456+
or "",
457+
("" if first_location is None else first_location.get("start_line", "")),
458+
("" if first_location is None else first_location.get("commit_sha", "")),
459+
str(alert["push_protection_bypassed"]),
460+
(
461+
""
462+
if alert.get("push_protection_bypassed_by") is None
463+
else alert["push_protection_bypassed_by"].get("login", "")
464+
),
465+
alert.get("push_protection_bypassed_at", ""),
466+
(
467+
""
468+
if alert.get("push_protection_bypass_request_reviewer") is None
469+
else alert["push_protection_bypass_request_reviewer"].get("login", "")
470+
),
471+
alert.get("push_protection_bypass_request_reviewer_comment", ""),
472+
alert.get("push_protection_bypass_request_comment", ""),
473+
alert.get("push_protection_bypass_request_html_url", ""),
474+
("" if alert.get("assigned_to") is None else alert["assigned_to"].get("login", "")),
319475
alert["repository"]["full_name"],
320476
alert["repository"]["owner"]["login"],
321477
alert["repository"]["owner"]["type"],

0 commit comments

Comments
 (0)