
Read-only Entra ID app-credential assessment: enumerates Graph permissions, Azure RBAC, and reachable cloud data, then maps findings to privilege-escalation and lateral-movement paths.
____ __ ____ __ ____
/ __/__ ______ ___ ___ / /_ / __/ / /____ _ / / /_____ ____
_\ \/ -_) __/ -_) -_)__/ __/ _\ \ / __/ _ `// / '_/ -_) __/
/___/\__/\__/\__/\__/ \__/ /___/ \__/\_,_//_/_/\_\\__/_/
╔╦╦╬╬╬╬╬╬╦╦╗
╔╬╬╬╝╝┘ ╚╝╝╬╬╬┐
╬╬╝╚╩╬╗╔ ╚╬╬╬
╬╝ ╚╬╬╗╗ ╔ ╚╬╗
╬╬ ╔╗ ╚╬╬╬╬╬╬╦ ╬╬ we found your secret...
╔╬┤ ╬╬╬ ╬╬╬╬╬╬╬╬╝╝╝╬╬╗ ...now let's see what it
╬╬┤ ╚╩┘ ╚╬╬╬╬╬╩ ╠╬╬ can REALLY do. ( o_o)>=|=
╬╬┤ ╠╬╬
╬╬ ╦╗ ╗╗ ╬╬ [ client_id + secret -> total recall ]
└╬┐ ╚╬╗╗ ╔╬╬╝ ╔╬┘
└╬╗ ╚╩╩╬╬╬╩╩╝╝ ╔╬╬
╚╬╬╬╗ ┌╗╬╬╝┘
╚╩╬╬╬╦╦╦╦╦╦╬╬╬╝╝
╚╚╝╝╝╝
// pst... that app registration talks too much. \\
What can this Entra ID client ID + secret actually do?
You found an Entra ID (Azure AD) application credential — a client ID and secret —
on an authorized engagement, and the tenant it belongs to is in scope.
secret_stalker takes those two values and tells you, from a cold start:
passwordCredentials and reports the expiration
date + days remaining (needs directory read; see note below).wids claim) and objects it owns
(apps/SPs you can add credentials to).It authenticates with either a client secret or a certificate (--cert),
and works against commercial and sovereign clouds (--cloud).
It is passive by default and never modifies anything — read-only enumeration only.
⚠️ Authorized testing only. Run it solely against tenants that are explicitly in scope for an engagement you are authorized to perform.
pip install -r requirements.txt # just runs it from source
# — or —
pip install . # installs the `secret_stalker` command
pip install '.[cert]' # + certificate (--cert) auth support
pip install '.[dev]' # + pytest for the test suite
The only runtime dependency is requests. Tokens are decoded locally (base64 +
JSON) — no signature verification, no crypto library, no Microsoft SDK. The one
exception is certificate auth (--cert), which needs the optional cryptography
package to sign the JWT client assertion. Requires Python 3.7+.
After pip install . you can invoke it as secret_stalker … instead of
python -m secret_stalker ….
The fastest way to find out what a credential can do:
python -m secret_stalker \
--tenant contoso.onmicrosoft.com \
--client-id 11111111-2222-3333-4444-555555555555 \
--secret '<client-secret>'
--tenant accepts either a tenant GUID or a domain — a domain is resolved to its
tenant ID automatically via the public OpenID configuration endpoint.
Pass credentials via environment variables instead of flags:
export SS_TENANT=contoso.onmicrosoft.com
export SS_CLIENT_ID=11111111-2222-3333-4444-555555555555
export SS_SECRET='<client-secret>'
python -m secret_stalker
Any of --tenant / --client-id / --secret can come from SS_TENANT /
SS_CLIENT_ID / SS_SECRET. Flags take precedence over the environment.
This isn't only about shell history: an argv value is readable by any local
user for the lifetime of the process (ps, /proc/<pid>/cmdline). If --secret
or --cert-password is passed as a flag, the tool prints a one-line reminder to
stderr — it never appears in --json or --export output.
App registrations often use a certificate rather than a secret. Pass --cert
(a PEM holding the private key and certificate, or a .pfx/.p12) and the
tool authenticates with a signed JWT client assertion:
python -m secret_stalker --tenant contoso.onmicrosoft.com \
--client-id <appId> --cert ./app.pem # or app.pfx
# encrypted key / PFX:
python -m secret_stalker ... --cert app.pfx --cert-password '<pw>'
Certificate auth needs the optional cryptography package (pip install '.[cert]').
The tool reports the certificate's own expiry (matched on its thumbprint in the
app's keyCredentials), just like it does for a secret. --cert/--cert-password
also read from SS_CERT / SS_CERT_PASSWORD.
By default secret_stalker targets the commercial cloud. For sovereign tenants,
pass --cloud (or SS_CLOUD) so the Entra authority and the Graph / ARM / Key
Vault endpoints match — otherwise valid credentials look like they have no access:
# US Government (GCC High)
python -m secret_stalker --cloud usgov --tenant contoso.onmicrosoft.us ...
# US DoD (L5)
python -m secret_stalker --cloud usdod ...
# Azure operated by 21Vianet (China)
python -m secret_stalker --cloud china --tenant contoso.partner.onmschina.cn ...
--cloud | Entra authority | Microsoft Graph | ARM | Key Vault |
|---|---|---|---|---|
public (default) | login.microsoftonline.com | graph.microsoft.com | management.azure.com | vault.azure.net |
usgov (GCC High) | login.microsoftonline.us | graph.microsoft.us | management.usgovcloudapi.net | vault.usgovcloudapi.net |
usdod (DoD) | login.microsoftonline.us | dod-graph.microsoft.us | management.usgovcloudapi.net | vault.usgovcloudapi.net |
china (21Vianet) | login.chinacloudapi.cn | microsoftgraph.chinacloudapi.cn | management.chinacloudapi.cn | vault.azure.cn |
Aliases like gov, dod, commercial, gcc-high, and 21vianet are accepted.
(The Storage data-plane audience, storage.azure.com, is the same in every cloud.)
Refresh the permission map for this tenant (once). Resolves Graph permission GUIDs to authoritative names for the target tenant:
python -m secret_stalker --update-manifest
This authenticates, caches the tenant's Graph appRole map to
~/.secret_stalker/app_roles_cache.json, and exits. Skip it if the
credential can't read service principals — the bundled map still covers the
well-known permissions.
Passive baseline. See validity, Graph permissions, and Azure RBAC without touching tenant objects:
python -m secret_stalker
Go active when you want depth + a report to keep. Adds read-only Graph object samples and Key Vault / Storage data-plane reachability, and writes a flattened export for triage or ingestion:
python -m secret_stalker --active --export results.ndjson
The terminal report is organized top-to-bottom from "does it work" to "what's the damage." A typical run looks like:
secret_stalker — credential assessment
======================================