GitHub - puck-security/geiger: Detection tells you a key is real; geiger tells you whether it's dangerous.

16 min read Original article ↗
geiger

Is it still live, what does it reach, and how bad? Read-only blast-radius triage for leaked credentials.

license: MIT release go 1.25+ recon: read-only

geiger triaging an AWS key — HIGH: prod account with secrets-manager access

Your secret scanner found the key — it won't tell you if it's still live or what it unlocks. geiger does: pipe any credential-bearing text at it and it recognizes the credentials inside, runs read-only recon with each, and ranks what they actually reach by blast radius.

Dual-use triage: an incident responder's "how bad is this?" and a pentester's "what does this key reach?". Read-only by construction, dry-run by default.


Install

Binary — grab the archive for your OS/arch from Releases:

tar xzf geiger_*_linux_amd64.tar.gz && sudo mv geiger /usr/local/bin/

Source (Go 1.25+):

git clone https://github.com/puck-security/geiger && cd geiger
go build -o geiger ./cmd/geiger

Tutorial

geiger doesn't touch anything on the network until you say so. Dry-run first (default): it recognizes the credential and prints the read-only calls it would make. Try it on AWS's well-known example keys — no real secret needed:

printf 'AWS_ACCESS_KEY_ID=AKIAIOSFODNN7EXAMPLE\nAWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY\n' | geiger

That prints the read-only calls it would run (sts:GetCallerIdentity, …). Add --live with a real credential to actually run them and get the impact note:

echo 'GITHUB_TOKEN=ghp_...' | geiger --live

How-to

# a file, stdin, or a cloud CLI's output
geiger --live .env
cat sso-cache.json | geiger --live
aws configure export-credentials | geiger --live

# the current environment
geiger --env --live

# a whole repo / dir (walked; results sorted by impact)
geiger --live ./leaked-repo

# a scanner's report — e.g. a TruffleHog sweep of a compromised laptop,
# exactly what supply-chain worms (Shai-Hulud) run; triage which creds reach prod
geiger --live --from-trufflehog trufflehog.json
geiger --live --from-gitleaks gitleaks-report.json

# rank by YOUR crown jewels (boost anything touching these to HIGH+)
geiger --live --context '1234567890,acme-prod,billing-service' ./repo

# self-hosted services need a host
echo 'VAULT_TOKEN=hvs....' | geiger --live --endpoint https://vault.internal:8200

# only what matters; save a clean artifact
geiger --live --min-severity high -o case-1234.txt ./repo

# OPSEC: identity call only; route egress through a proxy
geiger --live --min-footprint --proxy socks5://127.0.0.1:9050 .env

# machine-readable
geiger --live --json ./repo | jq .

Go deeper — --intrusive (doesn't modify resources, but leaves a trail): connects to databases (Postgres, MySQL, MongoDB, Redis, SQL Server, Oracle, ClickHouse, Cassandra — fixed catalog queries, read-only session), reads local SQLite/IDE stores in place, hits cluster APIs, redeems cached user refresh tokens (Azure / GCP sessions) to map their reach — an active sign-in that shows in the tenant's audit log — and follows secrets-store reads, draining Vault/Doppler/1Password/cloud secret managers (AWS SM, GCP SM, Azure Key Vault) and recursively triaging each extracted secret. The same fan-out a worm performs, so you see the real blast radius. Plain --live stays read-only: it uses a still-valid cached token but never redeems a refresh token.

geiger --live --intrusive .env

SSH keys — point it at a directory; it fingerprints each key (encrypted keys are locked, not dead). --ssh-correlate lists candidate target hosts from ~/.ssh/config, known_hosts, and shell history.

geiger --ssh-correlate ~/.ssh

Where geiger fits

geiger is not a scanner — it starts where they stop. Detection finds the secret; geiger triages it. Point gitleaks or TruffleHog at the haystack, then pipe the report in (--from-gitleaks / --from-trufflehog) to learn which hits actually reach prod.

gitleaks TruffleHog GitGuardian geiger
Find secrets in code / git / files ▶ consumes their report
Verify the secret is live
Characterize blast radius (identity, scope, reach) partial¹ partial² ✅ ~163 types, scored
Drain secret-managers + recursively triage downstream
DB / cluster / on-disk store recon (read-only)
Ranked for IR ("how bad, in what order") partial
Local, no SaaS / account — (SaaS)

¹ TruffleHog's analyze enumerates permissions for ~a dozen providers — the closest peer; geiger generalizes that to ~163 credential types with blast-radius scoring and downstream harvest. ² GitGuardian assigns validity/severity inside its platform.

Detection is their job and they're good at it — geiger doesn't replace them, it answers the question they leave open: now that you found it, how bad is it?


Reference

Flags

Flag Effect
(stdin / files / dirs) input source; multiple files/dirs may be passed, and a directory is walked
--live make read-only recon calls (default: dry-run)
--intrusive connect to DBs / cluster APIs, read local stores, harvest downstream secrets (needs --live)
--min-footprint identity call only; skip inventory fan-out
--env read current environment variables
--endpoint URL host/instance for self-hosted & set-shaped creds
--proxy URL route HTTP recon via http/https/socks5 proxy
--timeout DUR per-credential recon timeout (default 15s)
--concurrency N credentials reconned at once on --live (default 8)
--context TERMS comma-separated crown-jewel terms; a match raises tier
--min-severity TIER only print findings at or above a tier (critical/high/medium/low/info/dead); dead is the floor, so info excludes dead and high keeps only critical+high
-o, --output FILE write results to FILE instead of stdout (0600, color off; status stays on stderr)
--json machine-readable output (NDJSON, one note per line)
--stream print results as found (discovery order) instead of sorted by impact
--no-reverse keep highest-impact findings first; by default an interactive terminal reverses them to the bottom (above the summary) so the worst don't scroll off the top
--only TYPES / --skip TYPES scope by module name or category (databases,cloud,secrets,ai,vcs,kubernetes,identity,backup,endpoint)
--from-gitleaks F / --from-trufflehog F triage each finding in a scanner report
--ssh-correlate SSH: read local hints for candidate target hosts
--trace print the raw request + response of each call (secrets masked)
--user-agent UA User-Agent for recon calls (default geiger/<version>)
--color MODE auto (default, off when piped) / always / never
-v / -q show planned/executed calls (and full finding detail) / quiet stderr
--version print version

Tiers

CRITICAL · HIGH · MEDIUM · LOW · INFO · DEAD — a composite blast-radius score (capability × reach × sensitivity), relative not absolute. --context matches and force-multiplier capabilities force at least HIGH.

What geiger reads — and what it can't

geiger triages a credential you were handed, or one sitting on disk.

  • In scope — on-disk / offline-readable. API tokens, connection strings, cloud CLI caches (~/.aws, gcloud, MSAL), SSH keys, kubeconfigs, secrets-manager creds, MCP configs, AI-IDE plaintext token stores, password-manager recovery material (KeePass, encrypted Bitwarden — offline-crackable with the master password), plaintext exports, and Firefox saved logins (logins.json + key4.db), which decrypt offline when no primary password is set.
  • Out of scope — in-process / OS-bound. Chromium passwords & cookies (wrapped by DPAPI / macOS Keychain / Secret Service), raw DPAPI blobs, the macOS Keychain, LSASS. Reading those means decrypting against a live OS session — credential extraction from a host, not triage of one. Not always a black and white line.

Coverage

Recognition rides on gitleaks (shape/checksum) plus geiger's own shape/env-name recognizers; an unrecognized type is reported unknown, not characterized. Triage keys on capability — a key that runs code, wipes devices, restores backups, or reads other secrets is a force multiplier; a billed-usage API key is a warning.

Full coverage — 163 credential types (regenerate with go run ./tools/coverage)

Cloud & hosting

Credential / app Reach
aws AWS account — IAM-scoped access across all AWS services
aws_sso active SSO session
aws_sso_registration SSO client registration (no session)
gcp_service_account GCP service account — exchanged a read-only token
gcp_adc gcloud user credentials — delegated user access
azure_msal Azure CLI session — Entra identity, refreshable headlessly
digitalocean DigitalOcean — droplets, databases, Spaces, Kubernetes
digitalocean_oauth DigitalOcean OAuth — account API access
linode Linode — compute, storage, DNS account control
cloudflare Cloudflare — DNS/zones/Workers per token scope
cloudflare_global Cloudflare Global API Key — full account control
fastly Fastly — CDN config, edge purge/secrets
heroku Heroku — app deploy + config-vars (downstream secrets)
render Render — deploy (code exec) + env-secret access
railway Railway — deploy (code exec) + project-variable access
flyio Fly.io — machine deploy (code exec) + app-secret access
netlify Netlify — site deploy + build env vars
vercel Vercel — project deploy + env vars
tailscale Tailscale — tailnet device/ACL admin + auth-key minting
terraform_cloud Terraform Cloud — workspace state & variables (often secrets)
oci_config Oracle Cloud — API signing config (key_file referenced, not inline)

Source control & packages

Credential / app Reach
github_pat GitHub — repo read/write/admin, org membership, Actions
gitlab GitLab — repo/project access per token scope
gitlab_ci_token GitLab CI job token — pipeline-scoped repo/registry access
jfrog JFrog Artifactory — artifact read/write + package admin
docker_registry container registry — pull/push images (supply-chain)
npm npm — publish/yank packages (supply-chain)
pypi PyPI — publish packages (supply-chain)
rubygems RubyGems — publish gems (supply-chain)

CI/CD & build

Credential / app Reach
buildkite Buildkite — pipelines + build-agent access
circleci CircleCI — pipelines + project env vars

Databases & data platforms

Credential / app Reach
db_connection_string direct DB data-plane (pg/mysql/mongo/redis/mssql/oracle/clickhouse/cassandra/sqlite)
snowflake Snowflake — data-warehouse access (account-wide at ACCOUNTADMIN)
databricks Databricks — workspace, jobs, notebooks, data
mongodb_atlas MongoDB Atlas service account — Admin API access to orgs/projects/clusters
supabase Supabase service_role — RLS-bypass full DB + auth admin
planetscale PlanetScale service token — database & branch admin
neon Neon API key — Postgres project & role admin
aiven Aiven token — managed-service & credential admin
upstash Upstash management API — database & token admin
redis_cloud Redis Cloud API — subscription & database admin
clickhouse_cloud ClickHouse Cloud API key — service & member control
clickhouse_selfhosted ClickHouse (self-hosted) — SQL data & user access
plaid Plaid keys — bank-account data access (financial PII)

AI / LLM & agentic

Credential / app Reach
openai OpenAI — model access, files/fine-tunes; org-owner = key/billing admin
anthropic Anthropic — Claude model access (billed)
claude_code_oauth Claude Code / Claude subscription OAuth token — acts as the signed-in user
gemini Google Gemini — generative API (billed usage)
azure_openai Azure OpenAI — resource model access (billed usage)
cohere Cohere — LLM API access (billed)
mistral Mistral — LLM API access (billed)
replicate Replicate — model runs (billed) + model access
huggingface Hugging Face — model/dataset access; write token can push
groq / together / deepseek / openrouter / xai / fireworks LLM API access (billed usage)
perplexity Perplexity — LLM API (billed usage)
elevenlabs ElevenLabs — voice API (billed usage + voice library)
stability Stability AI — image API (billed usage)
pinecone Pinecone — vector index read/write (embedded data)
mcp_config MCP config — agent credential aggregator (extracts + re-triages embedded secrets)
ai_ide_store AI-IDE token store (Cursor/VS Code state.vscdb, plaintext)

Secrets managers & vaults

Credential / app Reach
vault HashiCorp Vault — read secrets per policy
conjur CyberArk Conjur — secrets-manager access (reads downstream secret values)
cyberark_pvwa CyberArk PVWA — privileged-account vault access
infisical Infisical — secret-store read across authorized projects
akeyless Akeyless — secret-store read (list-items / get-secret-value)
delinea_secret_server Delinea/Thycotic Secret Server — vaulted-secret read access
doppler Doppler — project config/secret access
onepassword_connect 1Password Connect — vault item access via the Connect server
onepassword_sa 1Password service account — vault secret access (CLI/SDK only)
onepassword_secret_key 1Password Secret Key — vault-unlock half; dangerous with the master password
bitwarden Bitwarden API key — enumerates the vault (items encrypted without the master password)
bitwarden_vault Bitwarden encrypted vault — offline-crackable with the master password
keepass_db KeePass vault — offline-crackable to every secret with the master password
vault_export_plaintext plaintext credential dump — full account takeover across every listed site

Identity, SSO & directory

Credential / app Reach
okta Okta — directory, SSO apps, admin per scope
pingone PingOne — identity & SSO admin
pingfederate PingFederate admin API — OAuth client & federation-trust control
entra_sp Microsoft Entra service principal — Graph/Azure per app roles
jumpcloud JumpCloud — directory, device & SSO admin
sailpoint SailPoint — identity governance (certs, provisioning)
auth0 Auth0 — tenant management API (users, apps, rules)
duo Duo — MFA admin API (bypass codes, user mgmt)
workday Workday — HR/finance records (PII)

Endpoint, MDM, RMM & config-mgmt

Credential / app Reach
ninjaone NinjaOne RMM — script execution + remote control across endpoints
atera Atera RMM/PSA — full agent & customer inventory
kandji Kandji MDM — device inventory + remote lock/erase
jamf Jamf Pro — script execution + MDM lock/wipe
mosyle Mosyle MDM — device control incl. remote wipe/lock
automox Automox — patch policies + worklet script execution across endpoints
tanium Tanium — package deploy + action execution across endpoints
ansible_awx Ansible AWX/Tower — playbook execution across managed hosts
puppet_enterprise Puppet Enterprise — orchestrator task/plan execution on nodes
saltstack SaltStack salt-api — arbitrary module execution across minions
fleet Fleet (osquery) — live queries + script execution + MDM wipe

Monitoring, observability & security posture

Credential / app Reach
datadog Datadog — metrics/logs/APM + monitor & key admin
newrelic New Relic — telemetry + account admin
grafana Grafana — dashboards/datasources; server-admin = full
splunk Splunk — search over all data + scripted-input command execution
sumologic Sumo Logic — log search (secrets/PII) + collector admin
dynatrace Dynatrace — observability data + config API
honeycomb Honeycomb — event data + query API
sentry Sentry — error events (may hold secrets/PII) + org admin
zabbix Zabbix — global script execution on monitored hosts
auvik Auvik — network topology & device inventory
manageengine_opmanager ManageEngine OpManager — device management + workflow script execution
lacework Lacework — cloud security posture across connected accounts
wiz Wiz — cloud security graph (findings, identities, secrets)
snyk Snyk — code/dependency vuln data + org project access

Backup & DR

Credential / app Reach
veeam Veeam Backup & Replication — job control + restores
acronis Acronis Cyber Protect — backup/restore + agent control
cohesity Cohesity — protection-job & restore control
netbackup Veritas NetBackup — policy & restore control
commvault Commvault — backup/restore + client management

ITSM, productivity & support

Credential / app Reach
servicenow ServiceNow — ITSM/CMDB records + workflows
jira Jira (Atlassian Cloud) — full issue/project read + user-directory PII
ivanti Ivanti ITSM — incident/CMDB access + employee PII
snipeit Snipe-IT — asset/license inventory + user PII
pagerduty PagerDuty — on-call schedules + incident API
linear Linear — issues/projects + member data
asana Asana — tasks/projects + workspace members
notion Notion — workspace pages/databases (often secrets/PII)
zendesk Zendesk — support tickets (customer PII)
intercom Intercom — conversations & customer profiles (PII)

Comms, email & SMS

Credential / app Reach
slack Slack — messages, files, users per token scope
discord_bot Discord bot — guild/message access per intents
telegram_bot Telegram bot — send/read in its chats
zoom Zoom — meetings, recordings, user admin (S2S OAuth)
twilio Twilio — SMS/voice send (billed) + message logs (PII)
vonage Vonage — SMS/voice send (billed)
sendgrid SendGrid — email send + template/contact access
mailgun Mailgun — email send + logs (recipient PII)
mailchimp Mailchimp — audience export (subscriber PII) + send
postmark Postmark — transactional email send + history
brevo Brevo — email/SMS + contact PII

Payments, CRM & SaaS data

Credential / app Reach
stripe Stripe — charges/customers/payouts (a live key moves money)
square Square — payments + customer data (financial)
coinbase Coinbase — account access (financial; can move funds)
salesforce Salesforce — CRM object access (customer PII)
hubspot HubSpot — CRM contacts/deals (PII)
shopify Shopify — store orders/customers (PII) + admin
klaviyo Klaviyo — customer-profile export (PII) + campaign send
braze Braze — message-send to all users + profile export (PII)
segment Segment — customer event-pipeline access (behavioral PII)
mixpanel / amplitude product-analytics data (behavioral PII)
customerio Customer.io — messaging + customer data (PII)
docusign DocuSign — envelopes/agreements (legal docs)
dropbox Dropbox — file access per scope
box Box — file/folder access; admin = all content
airtable Airtable — base data (often PII/secrets)
algolia Algolia — search index read/write; admin key = full
confluent Confluent Cloud — Kafka cluster & topic admin

Local credential stores & keys

Credential / app Reach
ssh_private_key SSH private key — local fingerprint only
kubeconfig kubeconfig — cluster credential
firefox_logins Firefox saved logins — offline-decrypted when no primary password
jwt decoded offline — map issuer to its provider for live recon
generic_secret unrecognized credential (matched by name)
needs_endpoint recognized — provide --endpoint to characterize

Plus the dev-laptop / supply-chain on-disk stores read natively: ~/.docker/config.json, ~/.npmrc, ~/.netrc, ~/.git-credentials, the gh CLI hosts.yml, ~/.databrickscfg, Snowflake connections.toml, the Terraform CLI credentials.tfrc.json, ~/.vault-token, Fly.io config.yml, ~/.oci/config, Terraform state, and AWS-INI / GCP-SSO-MSAL JSON caches.

Bug reports and PRs welcome.

Output

A block per credential: a tier, a redacted title with the source location, never the raw secret, labeled findings ( notable, ⚠⚠ force multiplier, ? can't-determine-read-only), and a one-line takeaway.

For IR, each finding leads with where and when: an exposure line classifies the source — a crash dump (in-memory, persisted to disk, often auto-uploaded — may have left the host), a VS Code local-history snapshot, an IDE secret store, shell history, a log — and source modified / validated live carry the file mtime and live-check timestamp. When a secret turns up in several files, also exposed in groups them by class (8 local-history snapshots; 7 crash dumps) instead of listing paths (the full list expands under -v and in --json).

With -v each planned call prints as a copy-pasteable curl. Triaging more than one credential prints a closing summary — tier breakdown, rotate-first queue, and follow-ups (secrets-store reach, what couldn't be characterized, anything hidden by --min-severity). GitHub write/admin and org-admin are read from each repo's permissions and /user/memberships/orgs, so they're reported even for fine-grained PATs that expose no scopes.

Drift resilience. Beyond declared field paths, every response is scanned heuristically (admin/owner indicators → force multiplier; a fallback identity + count when the API shape changed), so a module stays useful as providers rename fields. --trace shows the raw request/response (secrets masked).


How it works

Pipeline: recognize → (authenticate) → recon → note. Recon runs the identity/whoami call first, then a couple of count calls to size reach.

Safety model

  • Read-only by construction. One client allows only GET/HEAD plus a short allowlist of read-only POSTs (STS GetCallerIdentity, k8s SelfSubjectRulesReview, the single OAuth token exchange). DB recon uses a read-only session and a fixed query allowlist. Local stores (SQLite, IDE state.vscdb, Firefox key4.db) open read-only. A guard test enforces this across every module.
  • Dry-run by default. --live is required and always prints the destinations it hits (real provider APIs, and their audit logs).
  • Attribution. Recon identifies itself as geiger/<version> — dual use beware, no detection evasion; defenders can attribute the calls.
  • Secrets are not printed or stored. Redacted everywhere; scrubbed from URLs, headers, and errors.
  • Untrusted-input hardening. Refuses cloud-metadata targets (SSRF), sanitizes hostile API responses, strips file-referencing DSN options

Principle: likely impact, not perfect impact. geiger is triage, not deep cloud-privesc graphing (use PMapper/CloudFox/ScoutSuite/etc for that).

Authorized use only. geiger exercises live credentials — run it only on creds you are entitled to triage.


Contributing a module

Most providers are a few lines of declarative recipe:

add("digitalocean-pat", recipe.HTTP{
    ModuleName: "digitalocean", Base: "https://api.digitalocean.com",
    Auth:   recipe.AuthSpec{Kind: recipe.Bearer},
    Whoami: recipe.GET("/v2/account").Field("email", "account.email"),
    Calls:  []recipe.Call{recipe.GET("/v2/droplets").CountFrom("meta.total", "droplets")},
}.Module())

Add an httptest-backed test, then go run ./tools/coverage to refresh the coverage table above. Exotic signing (SigV4, RS256-JWT, Digest) implements the module.Module interface directly with the internal/sign + internal/auth helpers — see internal/modules/ for examples.