Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 9 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,8 @@ A note on common arguments: generally, the date in `--since` can be specified as
This script retrieves secret scanning alerts from GitHub repositories, organizations, or Enterprises and outputs them in CSV or JSON format. It supports filtering by state, date, and push protection bypass status. Use this to audit, analyze, or export secret scanning data for compliance or security purposes.

```text
usage: list_secret_scanning_alerts.py [-h] [--scope {ent,org,repo}] [--generic] [--bypassed] [--state {open,resolved}]
[--no-include-secret] [--include-locations] [--include-commit] [--since SINCE]
[--json] [--raw] [--quote-all] [--hostname HOSTNAME]
usage: list_secret_scanning_alerts.py [-h] [--scope {ent,org,repo}] [--no-generic] [--no-default] [--include-types INCLUDE_TYPES [INCLUDE_TYPES ...]] [--bypassed] [--state {open,resolved}]
[--no-include-secret] [--include-locations] [--include-commit] [--since SINCE] [--json] [--raw] [--quote-all] [--hostname HOSTNAME]
[--ca-cert-bundle CA_CERT_BUNDLE] [--no-verify-tls] [--quiet] [--debug]
name

Expand All @@ -44,24 +43,24 @@ options:
-h, --help show this help message and exit
--scope {ent,org,repo}
Scope of the query
--generic, -g Include generic secret types (not just vendor secret types/custom patterns, which is the
default)
--no-generic Exclude generic secret types from the output
--no-default Exclude default secret types from the output
--include-types INCLUDE_TYPES [INCLUDE_TYPES ...]
Include specific secret types in the output (adds to any generic/default secrets that are output, so use --no-generic and --no-default to exclude those if required)
--bypassed, -b Only show alerts where push protection was bypassed
--state {open,resolved}, -s {open,resolved}
--state, -s {open,resolved}
State of the alerts to query
--no-include-secret, -n
Do not include the secret in the output
--include-locations, -l
Include locations in the output
--include-commit, -c Include commit date and committer in the output
--since SINCE, -S SINCE
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
--since, -S SINCE 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
--json Output in JSON format (otherwise CSV)
--raw, -r Output the raw data from the GitHub API
--quote-all, -Q Quote all fields in CSV output
--hostname HOSTNAME GitHub Enterprise hostname (defaults to github.com)
--ca-cert-bundle CA_CERT_BUNDLE, -C CA_CERT_BUNDLE
--ca-cert-bundle, -C CA_CERT_BUNDLE
Path to CA certificate bundle in PEM format (e.g. for self-signed server certificates)
--no-verify-tls Do not verify TLS connection certificates (warning: insecure)
--quiet, -q Suppress non-error log messages
Expand Down
4 changes: 4 additions & 0 deletions githubapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ def list_secret_scanning_alerts(
scope: str = "org",
bypassed: bool = False,
generic: bool = False,
secret_types: list[str] | None = None,
progress: bool = True,
) -> Generator[dict, None, None]:
"""List secret scanning alerts for a GitHub repository, organization or Enterprise."""
Expand All @@ -438,6 +439,9 @@ def list_secret_scanning_alerts(
if generic:
query["secret_type"] = GENERIC_SECRET_TYPES

if secret_types:
query["secret_type"] = ",".join(secret_types)
Comment on lines 439 to +443
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When both generic=True and secret_types are provided, the secret_types parameter will overwrite the generic parameter value in the query dictionary (line 443 overwrites line 440). This means that if a user specifies both flags, only the specific types will be queried, and the generic types will be silently ignored. Consider either: (1) merging the values when both are provided, or (2) raising an error to indicate these options are mutually exclusive, or (3) documenting this behavior clearly.

Suggested change
if generic:
query["secret_type"] = GENERIC_SECRET_TYPES
if secret_types:
query["secret_type"] = ",".join(secret_types)
secret_type_list = []
if generic:
secret_type_list.extend(GENERIC_SECRET_TYPES.split(","))
if secret_types:
secret_type_list.extend(secret_types)
if secret_type_list:
query["secret_type"] = ",".join(secret_type_list)

Copilot uses AI. Check for mistakes.

alerts = self.query(
scope,
name,
Expand Down
60 changes: 48 additions & 12 deletions list_secret_scanning_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

"""List secret scanning alerts for a GitHub repository, organization or Enterprise."""

import itertools
import sys
import argparse
import logging
import datetime
import json
from typing import Generator, Any
from typing import Generator, Any, Iterable
from defusedcsv import csv # type: ignore
from githubapi import GitHub, parse_date
from requests.exceptions import HTTPError
Expand Down Expand Up @@ -142,7 +143,7 @@ def output_csv(results: list[dict], quote_all: bool) -> None:
LOG.info("Stopped by user")
return

def decorate_alerts(g: GitHub, alerts: Generator[dict[str, Any], None, None], include_locations: bool = False, include_commit: bool = False) -> Generator[dict[str, Any], None, None]:
def decorate_alerts(g: GitHub, alerts: Iterable[dict[str, Any]], include_locations: bool = False, include_commit: bool = False) -> Generator[dict[str, Any], None, None]:
"""Decorate alerts with additional information, for both the raw and make_result outputs.

Resolve locations and commit information, if that was asked for.
Expand Down Expand Up @@ -225,6 +226,8 @@ def list_secret_scanning_alerts(
bypassed: bool = False,
raw: bool = False,
generic: bool = False,
default: bool = False,
specific: list[str] | None = None,
Comment on lines 228 to +230
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change breaks backward compatibility for other scripts that call list_secret_scanning_alerts(). The files replay_secret_scanning_result_status.py (line 166) and resolve_duplicate_secret_scanning_alerts.py (line 207) call this function without the new default, generic, or specific parameters. Due to the bug mentioned in the previous comment where empty alerts are returned when all flags are False/None, these scripts will now return zero results. Consider either: (1) fixing the backward compatibility issue by defaulting to previous behavior when no flags are set, or (2) updating all calling scripts to explicitly pass the appropriate flags.

Copilot uses AI. Check for mistakes.
verify: bool | str = True,
progress: bool = True,
) -> Generator[dict[str, Any], None, None] | None:
Expand All @@ -235,18 +238,37 @@ def list_secret_scanning_alerts(
Output either the raw alert data, or flattened results.
"""
g = GitHub(hostname=hostname, verify=verify)
alerts = g.list_secret_scanning_alerts(
name, state=state, since=since, scope=scope, bypassed=bypassed, generic=generic, progress=progress
)

alerts = decorate_alerts(g, alerts, include_locations=include_locations, include_commit=include_commit)
alerts = []

if default:
default_alerts = g.list_secret_scanning_alerts(
name, state=state, since=since, scope=scope, bypassed=bypassed, generic=False, progress=progress
)

alerts.append(default_alerts)

if generic:
generic_alerts = g.list_secret_scanning_alerts(
name, state=state, since=since, scope=scope, bypassed=bypassed, generic=True, progress=progress
)

alerts.append(generic_alerts)

if specific:
specific_alerts = g.list_secret_scanning_alerts(
name, state=state, since=since, scope=scope, bypassed=bypassed, secret_types=specific, progress=progress
)
alerts.append(specific_alerts)

Comment on lines +242 to +263
Copy link

Copilot AI Nov 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When none of the flags (default, generic, specific) are set, the alerts list will be empty, and itertools.chain.from_iterable([]) will produce no results. This means the function will return no alerts when called with the default argument values (generic=False, default=False, specific=None). This breaks backward compatibility - previously the function would return default (non-generic) alerts when called without flags. Consider adding a check: if not (default or generic or specific), set default = True to maintain backward compatibility.

Copilot uses AI. Check for mistakes.
decorated_alerts = decorate_alerts(g, itertools.chain.from_iterable(alerts), include_locations=include_locations, include_commit=include_commit)

if raw:
for alert in alerts:
for alert in decorated_alerts:
yield alert
return None
else:
for alert in alerts:
for alert in decorated_alerts:
result = make_result(g, alert, scope, name, include_secret=include_secret, include_locations=include_locations, include_commit=include_commit)
if result is not None:
yield result
Expand All @@ -268,10 +290,20 @@ def add_args(parser: argparse.ArgumentParser) -> None:
help="Scope of the query",
)
parser.add_argument(
"--generic",
"-g",
"--no-generic",
action="store_true",
help="Exclude generic secret types from the output",
)
parser.add_argument(
"--no-default",
action="store_true",
help="Include generic secret types (not just vendor secret types/custom patterns, which is the default)",
help="Exclude default secret types from the output",
)
parser.add_argument(
"--include-types",
type=str,
nargs="+",
help="Include specific secret types in the output (adds to any generic/default secrets that are output, so use --no-generic and --no-default to exclude those if required)",
)
parser.add_argument(
"--bypassed",
Expand Down Expand Up @@ -374,7 +406,9 @@ def main() -> None:
include_locations = args.include_locations
include_commit = args.include_commit
bypassed = args.bypassed
generic = args.generic
generic = not args.no_generic
default = not args.no_default
specific = args.include_types
verify = True

if args.ca_cert_bundle:
Expand Down Expand Up @@ -402,6 +436,8 @@ def main() -> None:
bypassed=bypassed,
raw=args.raw,
generic=generic,
default=default,
specific=specific,
verify=verify
)

Expand Down
Loading