diff --git a/README.md b/README.md index c215b54..e4bc1dc 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 diff --git a/githubapi.py b/githubapi.py index 09c380b..1817755 100644 --- a/githubapi.py +++ b/githubapi.py @@ -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.""" @@ -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, diff --git a/list_secret_scanning_alerts.py b/list_secret_scanning_alerts.py index e428cca..de97ea4 100755 --- a/list_secret_scanning_alerts.py +++ b/list_secret_scanning_alerts.py @@ -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 @@ -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. @@ -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: @@ -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) alerts = decorate_alerts(g, alerts, include_locations=include_locations, include_commit=include_commit) @@ -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", @@ -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: @@ -402,6 +435,8 @@ def main() -> None: bypassed=bypassed, raw=args.raw, generic=generic, + default=default, + specific=specific, verify=verify )