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.26+):
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 (read-only, but it leaves a trail). It connects to
databases (Postgres, MySQL, MongoDB, Redis, SQL Server, Oracle, ClickHouse,
Cassandra) with fixed queries, reads local SQLite and IDE stores in place, and
calls cluster APIs. It follows secrets-store reads, draining Vault, Doppler,
1Password, and the cloud secret managers (AWS SM, GCP SM, Azure Key Vault) and
triaging each value. It also redeems cached Azure and GCP refresh tokens to map
their reach, which writes a sign-in to the audit log. This is the fan-out a worm
performs, so you see the real blast radius. Plain --live stays read-only and
never redeems a refresh token.
geiger --live --intrusive .env
Deeper, per service — --<service>-intrusive. Each major provider has a
read-only deep tier. It shows what a leaked credential can do, not only what it
can see. The flags table lists every flag. These tiers are separate from
--intrusive because they are call-heavy and specific to one provider.
AWS, GCP, Azure map the IAM blast radius: privilege-escalation paths,
assumable and cross-account roles, admin roles, and effective permissions.
Okta, Vault, GitHub, GitLab, Slack, Snowflake, Cloudflare size real reach:
user-directory and SSO-app access, secret-engine mounts, CI/CD secret names,
visible channels, reachable databases, and controlled domains.
Secret values are never printed; the CI-secret tiers return names only. AWS
--aws-assume mints STS credentials for an SSO session, keeps them in memory, and
scrubs them from output. The tiers that redeem a refresh token (GCP ADC, Azure)
write a provider sign-in log.
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
Agentic reach — point geiger at an agent config, a repo, or a home directory
and it maps what the agent's tool chain reaches. This is on by default: no flag,
it just happens when a config turns up in the walk.
The credential axis and the reach axis are separate, and that is the point. A
config whose every secret is stored properly — OS env, a keychain, an OAuth flow —
can still wire the agent to a filesystem server rooted at /, a shell server, and
the corporate wiki. Perfect credential hygiene bounds none of that.
geiger ~/.claude.json # what this agent's tools reach
geiger ./repo # .mcp.json / .cursor / .vscode in a repo
geiger --live ~/.cursor/mcp.json # enumerate remote servers' real tool lists
geiger --live --spawn-stdio ~/.claude # + run local stdio servers to enumerate them
Each server is typed into reach primitives — exec, corpus-search,
secrets-read, code-write, cloud-control, fs-read/fs-writewith their root
path, data-read, net-egress, untrusted-in, destructive, identity-admin.
Bulk store search is deliberately its own primitive: "give me every credential in
Confluence" is one tool call and the highest-yield move in a real engagement, so
folding it into a generic "reads data" would bury it.
That list is inventory, and it does not set a tier. A config file says what an
agent is wired to; it cannot say whether that is appropriate for the machine it
is on, and a filesystem server scoped to a project directory is the normal setup.
An ordinary developer laptop scores INFO.
The tier comes from the compositions — reach that exists only because the
tools share one context window, which no per-component scanner can see:
Chain
Why it matters
lethal trifecta
untrusted content, private data and a way out in one context — one poisoned page and the agent leaks what it can read
bulk read plus a way out
search the store, send the results. Two tool calls, no exploit
exec on this host
an exec tool makes the agent's reach the host's reach: every credential on the box, geiger's other findings in the same run included
reads a secret store
tool-chain access becomes credentials that outlive the session
shared context
a low-trust server sharing a context with a high-reach one
unpinned package
npx -y / uvx refetch at every launch, so the code that runs tomorrow need not be the code in the config
Alongside those, three things the file itself establishes: a filesystem root that
is the whole disk or home directory rather than a project, no approval prompt
(permissions.allow, alwaysAllow, YOLO mode), scored as a property of the whole
surface because it removes the human from every chain at once, and — under
--live only — a server that answers with no credential (enumeration sends
none, so the token in the config is not what gates it) or is reached over
plaintext. Hooks are inventoried too: they run shell on lifecycle events with
no model and no approval in that path, and nothing else lists them.
Runtimes read: Claude Code and Claude Desktop, Cursor, VS Code, Windsurf,
Cline/Roo/Kilo, Continue, Gemini CLI, Zed, Codex, Goose.
Honest by construction. The note always says where the reach came from: read
from the config (what these packages are known to do) or reported by the servers
themselves. A surface whose servers type to nothing at all reads UNKNOWN rather
than getting a severity invented from a guess.
The two flags split by transport. --live enumerates the remote servers'
real tool lists over http (tools/list, read-only; tools/call is never
issued). A local stdio server is only asked under --spawn-stdio, because
asking it means running the command in the config — a separate flag from
--intrusive on purpose: executing an argv that came out of the scanned file is
worse than anything --intrusive permits. Each server line says which it is
(stdio <command> or a URL) and whether it was asked.
Redact what geiger found — once you have rotated (or decided to rotate)
what a scan turned up, geiger can strip the values from the files it read, so a
backup directory or an old dotfile stops leaking the same credentials again:
geiger --dangerous-redact ~/.claude # plan only: what would change
geiger --dangerous-redact --confirm-redact ~/.claude # rewrite in place
geiger --dangerous-redact --confirm-redact --git-history \
--redact-replacements /tmp/repl.txt ./repo # + file for git filter-repo
git filter-repo --replace-text /tmp/repl.txt # then scrub the history
The plan lists each file, and under it each value: the line it sits on, the
module that recognized it, the variable or JSON path it came from, a masked
tail to match against the findings report, and the form it was found in. The
same list is printed after a rewrite, as the record of what changed. A plain
or JSON-escaped value is a text swap: the file keeps its shape and only the
value changes. A value stored as base64 (a docker auth blob, a kubeconfig
key) is replaced too, but the placeholder does not decode, so the consumer of
that file errors until the value is replaced. Anything else is listed under a
warning with the reason and is not rewritten: git history blobs (use the
replacements file), archive members, symlinks, binary and SQLite stores, values
harvested from a secrets store, and values that were transformed before geiger
saw them.
Two things it will not do. It keeps no backup: a backup would hold the secrets
again. And it revokes nothing: a redacted file is one that stops re-leaking,
not a rotated credential. It refuses to run on stdin, --env, --metadata,
--browser, or a scanner report, since none of those is a file it read itself.
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)
--spawn-stdio
enumerate local stdio MCP servers by running each configured command (needs --live); remote servers are enumerated by --live alone
--aws-intrusive
deep AWS IAM reads: full account authorization graph, self-grant unroll (groups + attached/inline policies), and role trust graph (needs --live; heavy CloudTrail)
--aws-assume SPEC
for an AWS SSO session, mint STS credentials and run the full key characterization per target: all, ACCOUNT, ACCOUNT/ROLE, or a comma-separated list (implies --aws-intrusive; needs --live)
--slack-intrusive
deeper Slack reach: workspace, users, channels, and files visible to the token (needs --live; the token's scopes are reported without it)
--github-intrusive
deeper GitHub reach: org member counts, org and repo Actions secret names, and installation-token repo reach (needs --live)
--gitlab-intrusive
deeper GitLab reach: project and group counts, maintainer/owner reach, and CI/CD variable keys (needs --live)
--gcp-intrusive
deeper GCP reach: probe effective IAM permissions (privilege escalation, code execution, data access) across reachable projects; redeems a gcloud ADC refresh token (needs --live)
--azure-intrusive
deeper Azure reach: Entra directory roles and Azure RBAC role assignments across subscriptions; redeems the az refresh token to mint Graph/ARM tokens; also probes an Entra service principal's app permissions (needs --live)
--okta-intrusive
deeper Okta reach: user directory read, SSO app inventory, and an admin-access probe (needs --live)
--vault-intrusive
deeper Vault reach: secret-engine mount inventory and path-capability probe (needs --live)
--snowflake-intrusive
deeper Snowflake reach: reachable databases and the roles the user holds (needs --live)
--cloudflare-intrusive
deeper Cloudflare reach: zone/domain names and Workers/R2 access (needs --live)
--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
--dangerous-redact
plan an in-place rewrite of the scanned files that replaces each found credential with REDACTED-BY-GEIGER; prints the plan, writes nothing. See Redact what geiger found
--confirm-redact
with --dangerous-redact, do the rewrite. No backup, no undo
--redact-replacements FILE
with --confirm-redact, also write a git filter-repo --replace-text file (0600) for scrubbing git history
--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 (also triaged for tool-chain reach, not just the secrets in
them — see Agentic reach), 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 — 178 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
google_oauth_client
Google OAuth client secret — valid; redeems this app's tokens
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
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).