Skip to content

nopcorn/patdown

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

patdown

A red team CLI for auditing what a GitHub token can do. Hand it any GitHub token (classic PAT, fine-grained PAT, OAuth user token, or a workflow GITHUB_TOKEN) and it reports the reach.

What it looks for

  • Token identity: owner, account type, 2FA status, emails, orgs, OAuth scopes, rate-limit headroom
  • Accessible repositories: visibility, archived state, secret + workflow-run counts (for workflow-based attacks), the token's collaborator role per repo
  • Inferred permissions: GitHub exposes no introspection endpoint for fine-grained PAT or GITHUB_TOKEN perms. Patdown probes a curated set of endpoints per scope (account / org / repo) and infers R, W, or RW from response status. Writes are detected only with -W.
  • Workflow-specific intel: for GITHUB_TOKEN only: Actions enablement, default workflow permissions, self-hosted runner count, environment list, OIDC subject format.

Why use it

  • Triaging a leaked token from a pentest or red-team find: figure out reach + impact in one command
  • Auditing your own CI workflow's permissions: block to confirm the runtime token can or can't do specific things
  • Pre-engagement recon when handed a token of unknown provenance

Design notes

  • Read probes are pure GETs. No mutation, no audit-log noise.
  • Write probes (-W) send real POST/PUT/PATCH requests with deliberately invalid bodies. They appear in org audit logs. See Write probe semantics below for the exact endpoints and status interpretation.
  • Concurrency is bounded (default 5) to stay under GitHub's rate-limit threshold.
  • ghr_ refresh tokens and ghu_ user-to-server tokens are rejected at startup since neither is usable directly against the GitHub REST API.

Supported tokens

Prefix Type Auto-probes permissions?
ghp_ Classic PAT no (scopes already exposed via X-OAuth-Scopes)
gho_ OAuth user token no
github_pat_ Fine-grained PAT yes
ghs_ GITHUB_TOKEN (Actions runtime) yes

Install

Patdown is distributed via GitHub only. Pick one:

# pipx (isolated env)
pipx install git+https://github.com/nopcorn/patdown.git

# plain pip (current env)
pip install git+https://github.com/nopcorn/patdown.git

# uvx (ephemeral)
uvx --from git+https://github.com/nopcorn/patdown.git patdown --help

To upgrade later: pipx upgrade patdown (or pip install --upgrade git+...).

Quick start

# token from env
export GH_TOKEN=ghp_xxx
patdown                          # enumerate everything
patdown owner/name               # focus on one repo
patdown owner/name -W            # also probe writes

# token from clipboard / pipe
pbpaste | patdown --token-stdin owner/name

# structured output
patdown --json | jq .

Permission coverage

GITHUB_TOKEN (ghs_*)

All 15 canonical permissions: keys are tracked. 13 are probed via REST/GraphQL/packages; id_token and models cannot be probed safely (OIDC issuance requires the runner; models.github.ai is a separate host).

Fine-grained PAT (github_pat_*)

24 repo perms, 14 account perms, 13 org perms (per unique org owning an accessible repo).

Gaps (intentionally not probed):

  • metadata: implicit, required for any /repos/... call to succeed
  • workflows: write-only, no read endpoint
  • issues:write: both POST /issues (422) and PATCH /issues/0 (404) are indistinguishable from "granted"
  • codespaces_lifecycle_admin: no distinct read endpoint
  • merge_queues: branch-scoped only (could support this in the future)
  • single_file: path-restricted; not distinguishable from contents
  • copilot_chat, knowledge_bases: no documented public REST endpoint

Classic PAT (ghp_*) / OAuth user token (gho_*)

These tokens use OAuth scopes, not granular fine-grained perms. The scope list is returned by GitHub in the X-OAuth-Scopes response header on every call, so there is no probe pass.

Scope hierarchy: admin:* includes write:* which includes read:* for the same subject. repo is a superset of public_repo, repo:status, repo_deployment, repo:invite, security_events, and (private-repo aspects of) workflow.

No permission probe table is rendered for these tokens as the scopes alone are authoritative.

Usage

usage: patdown [-h] [--version] [-W] [-j CONCURRENCY] [--max-repos MAX_REPOS]
               [--include-public] [--json] [--token-stdin] [-v] [-q]
               [repo]

Enumerate what a GitHub token can reach.

positional arguments:
  repo                  owner/name to focus on. Omit to enumerate every
                        accessible repo.

options:
  -h, --help            show this help message and exit
  --version             show program's version number and exit
  -W, --check-writes    Probe write perms (GITHUB_TOKEN or fine-grained PAT).
                        Real POSTs/PUTs with invalid bodies - audit-log
                        visible.
  -j, --concurrency CONCURRENCY
                        Concurrent probe workers (default: 5). Higher = faster
                        but more secondary-rate-limit risk.
  --max-repos MAX_REPOS
                        Cap repos enumerated/probed (default: no cap). No-op
                        when a repo is given.
  --include-public      Include pull-only public repos when enumerating. No-op
                        when a repo is given.
  --json                Emit JSON to stdout instead of human tables.
  --token-stdin         Read token from stdin (first line).
  -v, --verbose         Debug logging.
  -q, --quiet           Suppress info logs.

Token is read from $GH_TOKEN, $GITHUB_TOKEN, or --token-stdin.

Examples:
  GH_TOKEN=ghp_xxx patdown                          # enumerate everything
  GH_TOKEN=ghp_xxx patdown owner/name               # focus on one repo
  GH_TOKEN=ghp_xxx patdown owner/name -W            # also probe writes
  pbpaste | patdown --token-stdin owner/name        # paste token from clipboard
  GH_TOKEN=ghp_xxx patdown --json | jq .            # structured output

Token types recognized: classic PAT (ghp_), fine-grained PAT (github_pat_),
OAuth user token (gho_), GITHUB_TOKEN / Actions install (ghs_).

Output

Human mode prints three rich tables to stdout (token info, repo targets, permissions). JSON mode dumps a single object with all the same info.

  • Token Information: type, owner/login, 2FA, emails, orgs, scopes, rate limit. Empty rows are omitted.
  • Repository Targets: repo enumeration with secret/run counts, or a workflow-specific summary (Actions enablement, default workflow perms, runners, environments, OIDC subject).
  • Permissions: one row per scope (<account>, <org:foo>, owner/repo). Granted column uses R, W, RW. Writes are bolded red. Un-granted perms are omitted.

Write probe semantics

Each write probe sends a real authenticated POST/PUT with {} or an invalid body:

  • 422 → granted (validation rejected before any mutation)
  • 403 / 404 → denied
  • 201 / 200 → accidental success (flagged as side-effect)
  • 410 → feature disabled
  • Other → inconclusive

Caveats: writes appear in org audit logs; archived repos skip writes (403 regardless); pull_requests:create uses a nonexistent head ref so no PR lands. A 403 "not permitted to create" indicates the PR-creation gate is set on the org/repo, separate from missing perm.

Layout

patdown/
  cli.py             argparse entry point
  github_wrapper.py  token classification + HTTP session
  constants.py       probe tables
  probes.py          REST / GraphQL / packages probe helpers
  concurrency.py     parallel() fan-out + get_json()
  collectors.py      perm gathering + write classification
  repos.py           repo listing / pagination
  render.py          rich-table emitters
  auditor.py         Auditor orchestrator
tests/               pytest classification fixtures

Contributing

PRs welcome. pip install -e '.[test]' && pytest to run tests.

About

A red team CLI for auditing what a GitHub token can do. Hand it any GitHub token and it returns a structured picture of what you can do with it.

Resources

License

Stars

Watchers

Forks

Contributors

Languages