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)

alerts = self.query(
scope,
name,
Expand Down
53 changes: 44 additions & 9 deletions list_secret_scanning_alerts.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
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 +142,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 +225,8 @@ def list_secret_scanning_alerts(
bypassed: bool = False,
raw: bool = False,
generic: bool = False,
default: bool = False,
specific: list[str] | None = None,
verify: bool | str = True,
progress: bool = True,
) -> Generator[dict[str, Any], None, None] | None:
Expand All @@ -235,9 +237,28 @@ 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 = []

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 +241 to +261
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 code appends generators to a list, but decorate_alerts expects a flat iterable of alert dictionaries. When decorate_alerts iterates over alerts, it will iterate over the generator objects themselves, not the alert dictionaries within them.

Consider using itertools.chain to chain the generators together:

from itertools import chain

alert_generators = []

if default:
    default_alerts = g.list_secret_scanning_alerts(...)
    alert_generators.append(default_alerts)

if generic:
    generic_alerts = g.list_secret_scanning_alerts(...)
    alert_generators.append(generic_alerts)

if specific:
    specific_alerts = g.list_secret_scanning_alerts(...)
    alert_generators.append(specific_alerts)

alerts = chain.from_iterable(alert_generators)

Copilot uses AI. Check for mistakes.
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@copilot open a new pull request to apply changes based on this feedback


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

Expand Down Expand Up @@ -268,10 +289,20 @@ def add_args(parser: argparse.ArgumentParser) -> None:
help="Scope of the query",
)
parser.add_argument(
"--generic",
"-g",
"--no-generic",
action="store_true",
help="Include generic secret types (not just vendor secret types/custom patterns, which is the default)",
help="Exclude generic secret types from the output",
)
parser.add_argument(
"--no-default",
action="store_true",
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 +405,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 +435,8 @@ def main() -> None:
bypassed=bypassed,
raw=args.raw,
generic=generic,
default=default,
specific=specific,
verify=verify
)

Expand Down
Loading