When a leaked credential turns up or a device is compromised, the critical question
is is it still live, what does it reach, and how bad? Your secret scanner might
have found the key but it won't answer that.
geiger does. Pipe credential-bearing text at it: it recognizes the credentials
inside, runs read-only recon with each, and prints what it is and what it can
reach — ranked 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 and 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 anythin on the network until you say so. Dry-run first
(default): recognizes the credential and prints the read-only calls it would
make.
Add --live 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 (still read-only): 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, 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.
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
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
--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
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.
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.
Command geiger triages leaked credentials: it recognizes the credentials in piped text, a file, the environment, a directory, or a scanner report, runs read-only recon with each, and prints a short note on what the credential is and what it can reach.
Command geiger triages leaked credentials: it recognizes the credentials in piped text, a file, the environment, a directory, or a scanner report, runs read-only recon with each, and prints a short note on what the credential is and what it can reach.
Package color provides terminal coloring that is a no-op unless enabled, so output stays clean when piped or redirected (codes would otherwise pollute files and tools like jq).
Package color provides terminal coloring that is a no-op unless enabled, so output stays clean when piped or redirected (codes would otherwise pollute files and tools like jq).
Package parse turns raw input (a file, stdin, or the environment) into a Blob: the original text plus a flattened key/value view and any structured form (JSON object, INI sections).
Package parse turns raw input (a file, stdin, or the environment) into a Blob: the original text plus a flattened key/value view and any structured form (JSON object, INI sections).