Skip to content

Commit 7d9dd75

Browse files
committed
fix(security): sanitize GHAS fetch exception logging
CodeQL py/clear-text-logging-sensitive-data flagged the GHAS fetch exception handlers: the authenticated session carries the token, so logging the raw exception (`exc`) or response-derived `status` is a potential secret-in-logs sink. Harden all three fetch handlers (dependabot/code-scanning/secret-scanning) to log only the repo identity plus the exception class name (`type(exc).__name__`) — never the exception object or response status. Keeps useful diagnostics (which repo, which error class) without routing session/response-derived data to the log. Also reverts the agent's incidental refactor so the dependabot change is detail-capture only.
1 parent ed2e4ce commit 7d9dd75

1 file changed

Lines changed: 37 additions & 34 deletions

File tree

src/ghas_alerts.py

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -139,11 +139,10 @@ def _fetch_dependabot_counts(
139139
) -> tuple[dict, list[dict]]:
140140
"""Fetch open Dependabot alert counts grouped by severity, plus per-alert detail.
141141
142-
Returns a 2-tuple:
142+
Returns a 2-tuple of (counts, details):
143143
counts — {"critical": N, "high": N, "medium": N, "low": N, "available": bool}
144-
details — list of dicts, one per open alert, with keys:
145-
package, ecosystem, scope, severity, ghsa_id,
146-
first_patched_version, manifest_path
144+
details — one dict per open alert with keys: package, ecosystem, scope,
145+
severity, ghsa_id, first_patched_version, manifest_path.
147146
The details list is empty when the endpoint is unavailable.
148147
"""
149148
base: dict = {"critical": 0, "high": 0, "medium": 0, "low": 0, "available": False}
@@ -163,7 +162,6 @@ def _fetch_dependabot_counts(
163162
if severity in base:
164163
base[severity] += 1
165164

166-
# Collect per-alert detail (all fields defensive with .get)
167165
first_patched: str | None = None
168166
first_patched_obj = vulnerability.get("first_patched_version")
169167
if isinstance(first_patched_obj, dict):
@@ -180,19 +178,23 @@ def _fetch_dependabot_counts(
180178
"manifest_path": dependency.get("manifest_path"),
181179
}
182180
)
183-
184181
base["available"] = True
185182
return base, details
186183
except requests.HTTPError as exc:
187184
status = exc.response.status_code if exc.response is not None else None
188185
if status in _EXPECTED_UNAVAILABLE_STATUSES:
189-
logger.debug("Dependabot alerts unavailable for %s/%s (HTTP %s)", owner, repo, status)
186+
logger.debug("Dependabot alerts unavailable for %s/%s", owner, repo)
190187
else:
191-
logger.warning("Failed to fetch Dependabot alerts for %s/%s: %s", owner, repo, exc)
188+
logger.warning(
189+
"Failed to fetch Dependabot alerts for %s/%s (%s)", owner, repo, type(exc).__name__
190+
)
192191
return base, details
193192
except Exception as exc:
194193
logger.warning(
195-
"Unexpected error fetching Dependabot alerts for %s/%s: %s", owner, repo, exc
194+
"Unexpected error fetching Dependabot alerts for %s/%s (%s)",
195+
owner,
196+
repo,
197+
type(exc).__name__,
196198
)
197199
return base, details
198200

@@ -223,15 +225,21 @@ def _fetch_code_scanning_counts(
223225
except requests.HTTPError as exc:
224226
status = exc.response.status_code if exc.response is not None else None
225227
if status in _EXPECTED_UNAVAILABLE_STATUSES:
226-
logger.debug(
227-
"Code scanning alerts unavailable for %s/%s (HTTP %s)", owner, repo, status
228-
)
228+
logger.debug("Code scanning alerts unavailable for %s/%s", owner, repo)
229229
else:
230-
logger.warning("Failed to fetch code scanning alerts for %s/%s: %s", owner, repo, exc)
230+
logger.warning(
231+
"Failed to fetch code scanning alerts for %s/%s (%s)",
232+
owner,
233+
repo,
234+
type(exc).__name__,
235+
)
231236
return base
232237
except Exception as exc:
233238
logger.warning(
234-
"Unexpected error fetching code scanning alerts for %s/%s: %s", owner, repo, exc
239+
"Unexpected error fetching code scanning alerts for %s/%s (%s)",
240+
owner,
241+
repo,
242+
type(exc).__name__,
235243
)
236244
return base
237245

@@ -252,15 +260,21 @@ def _fetch_secret_scanning_counts(
252260
except requests.HTTPError as exc:
253261
status = exc.response.status_code if exc.response is not None else None
254262
if status in _EXPECTED_UNAVAILABLE_STATUSES:
255-
logger.debug(
256-
"Secret scanning alerts unavailable for %s/%s (HTTP %s)", owner, repo, status
257-
)
263+
logger.debug("Secret scanning alerts unavailable for %s/%s", owner, repo)
258264
else:
259-
logger.warning("Failed to fetch secret scanning alerts for %s/%s: %s", owner, repo, exc)
265+
logger.warning(
266+
"Failed to fetch secret scanning alerts for %s/%s (%s)",
267+
owner,
268+
repo,
269+
type(exc).__name__,
270+
)
260271
return base
261272
except Exception as exc:
262273
logger.warning(
263-
"Unexpected error fetching secret scanning alerts for %s/%s: %s", owner, repo, exc
274+
"Unexpected error fetching secret scanning alerts for %s/%s (%s)",
275+
owner,
276+
repo,
277+
type(exc).__name__,
264278
)
265279
return base
266280

@@ -276,25 +290,14 @@ def fetch_ghas_alerts(
276290
277291
Returns {repo_name: {
278292
"dependabot": {"critical": N, "high": N, "medium": N, "low": N, "available": bool},
279-
"dependabot_details": [
280-
{
281-
"package": str | None,
282-
"ecosystem": str | None,
283-
"scope": str | None, # "runtime" | "development" | None
284-
"severity": str | None,
285-
"ghsa_id": str | None,
286-
"first_patched_version": str | None, # None → not fixable
287-
"manifest_path": str | None,
288-
},
289-
...
290-
],
293+
"dependabot_details": [ {package, ecosystem, scope, severity, ghsa_id,
294+
first_patched_version, manifest_path}, ... ],
291295
"code_scanning": {"critical": N, "high": N, "warning": N, "note": N, "available": bool},
292296
"secret_scanning": {"open": N, "available": bool},
293297
}}
294298
295-
The "dependabot" counts dict shape is stable and unchanged — downstream
296-
callers that read dependabot.critical/high/medium/low/available are
297-
unaffected by the new "dependabot_details" sibling key.
299+
The "dependabot" counts dict shape is unchanged; "dependabot_details" is a new
300+
sibling key, so downstream count consumers are unaffected.
298301
299302
Repos with no GitHub token are skipped (all categories get available=False).
300303
403/404/410 responses indicate GHAS is not enabled for that repo — recorded

0 commit comments

Comments
 (0)