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.
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:
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
# 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 .env
SSH 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 hints
Browser 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 SIEM
Where geiger fits
geiger 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.
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.
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
--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
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.
(--browser models 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.)
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 — 175 credential types (regenerate with go run ./tools/coverage)
Cloud & hosting
Credential / app
Reach
aws
AWS account — IAM-scoped access across all AWS services
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
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.
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, --endpoint outranks 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.
Contributing a module
Most providers are a few lines of declarative recipe:
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.
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 imds harvests credentials from cloud instance-metadata services (AWS IMDS, GCP/Azure metadata, Kubernetes in-cluster SA, Alibaba, DigitalOcean, OCI) and normalizes each into a synthetic dotenv blob that geiger's normal recognizers pick up.
Package imds harvests credentials from cloud instance-metadata services (AWS IMDS, GCP/Azure metadata, Kubernetes in-cluster SA, Alibaba, DigitalOcean, OCI) and normalizes each into a synthetic dotenv blob that geiger's normal recognizers pick up.
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).