Is it still live, what does it reach, and how bad? Read-only blast-radius triage for leaked credentials.
Your secret scanner found a key but 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.
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.
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/geigergeiger 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' | geigerThat 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# 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
# this box's own cloud identity — harvest the instance-metadata credential
# (AWS instance role, GCP/Azure managed identity, k8s in-cluster SA, …) and
# triage what it reaches. The post-exploitation question, answered read-only.
geiger --metadata --live
geiger --metadata --live --intrusive # + in-cluster k8s RBAC, secrets-store drain
# 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
# external recon: pipe a nuclei exposure scan straight in. Its templates pull the
# leaked value out of each exposed endpoint (/.env, phpinfo, instance metadata);
# geiger types, validates, and ranks it, and records the URL it leaked from. It
# also parses the response body when present, reassembling multi-field creds (an
# AWS key+secret pair, a connection string) the flat extracted-results can't —
# run nuclei with -irr to include the body.
# Stream over a pipe so live secrets never land on disk (add -o only if you must).
nuclei -t exposures/ -l targets.txt -j -irr | geiger --live --from-nuclei -
# 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 .envSSH keys — point it at a directory; it fingerprints each key (encrypted keys
are locked, not dead). With --live it confirms the key's main use — git access
— by attempting an SSH login to GitHub/GitLab/Bitbucket and reporting the account
it authenticates as (or the repo, for a deploy key); a user key that logs in means
pull/push to that account's private repos (supply-chain risk). --ssh-correlate
adds candidate target hosts from ~/.ssh/config, known_hosts, and shell history.
geiger --live ~/.ssh # fingerprint + test GitHub/GitLab/Bitbucket access
geiger --ssh-correlate ~/.ssh # + guess other target hosts from local hintsBrowser extensions — --browser models the impact of a malicious Chromium-family
(Chrome, Edge, Brave, Chromium, Vivaldi) extension (CursedChrome-style proxy, infostealer, sideloaded MV3). It scores each
installed extension's permission union — cookies + broad host access + request
interception / script injection / proxy = read every site's session cookies and
pivot through the browser — and flags sideloaded/unpacked ones (unsigned, not
content-verified). With --live --intrusive it also inventories the live sessions
such an extension would reach from the Cookies store metadata (domains only — the
values are keychain-encrypted), ranked by blast radius (IdP/SSO sessions first).
Because an unsigned all-sites extension is capability-identical to a real
CursedChrome, geiger doesn't guess intent — for each flagged sideloaded extension
it builds a responder triage bundle: install age, UI surface, dev-project
markers, the extension id, and a low-false-positive grep of its on-disk code (and,
under --intrusive, its LevelDB storage) for hardcoded remote hosts (websocket /
public-IP endpoints) to verify against egress/DNS logs. Everything is emitted
as clean IOCs in --json (detail arrays) for a SIEM.
By default only extensions with risky reach are shown; narrow/benign ones are
collapsed into a count. --all lists every installed extension (a full inventory).
geiger --browser # score extensions + triage bundle
geiger --browser --live --intrusive # + session inventory + storage IOC grep
geiger --browser --all # full inventory (incl. narrow/benign)
geiger --browser --json # IOCs (extension id, hosts) for a SIEMgeiger layers on top of the scanners you already run: detection finds the secret,
geiger characterizes it. Pipe in a report (--from-gitleaks / --from-trufflehog
/ --from-nuclei -), or point it at a directory — which is how IR usually uses it.
| gitleaks | TruffleHog | GitGuardian | Kingfisher | geiger | |
|---|---|---|---|---|---|
| Find secrets in code / git / files | ✅ | ✅ | ✅ | ✅ | walks a tree; consumes their reports |
| Verify the secret is live | — | ✅ | ✅ | ✅ | ✅ |
| Characterize blast radius (identity, scope, reach) | — | partial | partial | ✅ | ✅ ~170+ types |
| Triage any credential you hand it, type auto-detected | — | — | — | partial | ✅ |
| Drain secret-managers + recursively triage downstream | — | — | — | — | ✅ |
| DB / cluster / on-disk store recon (read-only) | — | — | — | partial | ✅ |
| Ranked for IR ("how bad, in what order") | — | — | partial | ✅ | ✅ |
| Local, no SaaS / account | ✅ | ✅ | — (SaaS) | ✅ | ✅ |
Offline triage runs locally in seconds. A live run is dominated by provider
round-trip, so it scales with --concurrency and rate limits, not corpus size.
| measured | |
|---|---|
| Dry-run, 20k files / 148MB | ~2.9s warm, ~8s cold |
| Dry-run, a few files | 20–30ms |
| Calls per credential | identity + small inventory fan-out (AWS key: 4) |
--min-footprint |
1 call |
--intrusive is slower due to DB handshakes, secret-manager fan-out,
recursive triage.
| 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 |
--metadata |
harvest this instance's metadata credential (AWS/GCP/Azure/k8s/Alibaba/DigitalOcean/OCI) and triage it; requires --live (it's a network read) |
--browser |
model malicious-browser-extension impact: score installed Chromium-family extensions; with --live --intrusive, inventory the live sessions they'd reach |
--all |
with --browser, list every extension (not just the risky ones) |
--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/unknown/dead); dead is the floor, so info excludes dead and unknown, 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) |
--sarif |
SARIF 2.1.0 output for code-scanning and triage viewers; tier and score ride in properties (NDJSON stays canonical) |
--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 |
--from-nuclei F |
triage each value extracted by a nuclei JSONL (-j) scan; F = - reads stdin (stream over a pipe) |
--from-kingfisher F |
triage each finding in a Kingfisher JSON/JSONL report (not --redacted — geiger needs the value); F = - reads stdin. Their finding fingerprint is carried through to --json/--sarif so their viewer dedupes against its own findings |
--git-history |
also scan blobs in a repository's git history — catches credentials deleted from the working tree but still recoverable from the repo (needs git on PATH) |
--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 |
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.
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.
(
--browsermodels the extension-capability and session-metadata angle — which domains have a live session, which extension could reach them — without decrypting the cookie values; see Browser extensions above.)
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 — 175 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 |
gcp_metadata |
GCP instance service account — token-scoped reach |
azure_msal |
Azure CLI session — Entra identity, refreshable headlessly |
alibaba |
Alibaba Cloud RAM credential |
oci_instance_principal |
OCI instance principal |
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 |
groq — LLM API access (billed usage) |
together |
together — LLM API access (billed usage) |
deepseek |
deepseek — LLM API access (billed usage) |
openrouter |
openrouter — LLM API access (billed usage) |
xai |
xai — LLM API access (billed usage) |
fireworks |
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 |
ai_ide_store |
AI-IDE token store (plaintext SQLite) |
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 — valid; enumerates the vault (items stay 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
| 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) |
freshservice |
Freshservice ITSM key — tickets, requester PII and CMDB |
freshdesk |
Freshdesk help desk key — tickets and contact PII |
freshchat |
Freshchat — all conversations and contact PII |
freshsales |
Freshsales CRM — contact PII and pipeline data |
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 |
Mixpanel — product-analytics data (behavioral PII) |
amplitude |
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 — confirmed git host access |
kubeconfig |
kubeconfig — cluster credential |
firefox_logins |
Firefox saved logins — offline-decryptable on-disk store |
jwt |
decoded offline — no network call made; map issuer to its provider for live recon |
generic_secret |
unrecognized credential (matched by name) |
needs_endpoint |
recognized — provide --endpoint to characterize |
Uncategorized (add to a group in tools/coverage)
| Credential / app | Reach |
|---|---|
atlassian |
Atlassian API token — set email + site to validate reach |
bedrock |
Amazon Bedrock API key — foundation-model access (billable) |
confluence |
Confluence (Atlassian Cloud) — full space/page read; pages often hold secrets |
filestack |
Filestack API key — file upload/transform on this account |
workos |
WorkOS API key — SSO/Directory Sync/User Management control |
175 credential types
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).
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/HEADplus a short allowlist of read-only POSTs (STSGetCallerIdentity, k8sSelfSubjectRulesReview, the single OAuth token exchange). DB recon uses a read-only session and a fixed query allowlist. Local stores (SQLite, IDEstate.vscdb, Firefox key4.db) open read-only. A guard test enforces this across every module. - Dry-run by default.
--liveis 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
- Endpoint provenance. A host read out of scanned data is untrusted: a planted
URL would otherwise aim a real credential at whoever planted it. Every module
declares where its credential may go, enforced centrally at recognition time —
a SaaS module is pinned to its vendor's domains,
--endpointoutranks anything in the file, and a violation degrades to "needs endpoint" instead of dialing. Self-hosted services can legitimately live at any domain, so those are not pinned; their destination is flagged in the note instead.
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.
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())If your Base is templated on a host — {endpoint}, {host}, {api},
{server} — you must also declare an Endpoint policy. That host comes from
the file being scanned, which an attacker may have written, so geiger needs to
know which hosts are legitimate for your service:
// SaaS-only: pin the vendor's domains (list every region and gov host).
ModuleName: "zendesk", Endpoint: saasOnly("zendesk.com"), Base: "{endpoint}",
// Deployable at any domain — including vendors shipping both SaaS and
// on-prem, where pinning would break real deployments.
ModuleName: "vault", Endpoint: selfHosted, Base: "{endpoint}",Resolve the host with resolveEndpoint rather than reading a variable yourself:
it puts the operator's --endpoint ahead of anything in the file. Don't pair a
service's credential with another service's URL variable — bind each host
variable to the module it names. TestEveryEndpointSteeredModuleDeclaresAPolicy
fails if a module skips this.
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.