Skip to content
KitploitKITPLOIT
ToolsExploitsBlog
Log in
Submit
ToolsExploitsBlog
Submit

Hacking, PenTest, and Cybersecurity Tools for Your Security Arsenal!

Kitploit is a directory of hacking, cybersecurity, and pentesting tools. Discover the latest project updates to find vulnerabilities, analyze systems, automate testing, and strengthen your security.

··Feeds·Contact·Privacy·© 2026 Kitploit

Tool Directory

Categories

View all categories
Loading categories
SecretsStalker — 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. | Kitploit
Tools/GitHubGitHub/rootsecdev/secretsstalker
Authentication & AuthorizationCloud Infrastructure SecurityPrivilege EscalationReconnaissanceLateral MovementPost-ExploitationPenetration TestingCloud SecurityIdentity & Access Management (IAM)Red Teaming
341231 month agoReviewed by Kitploit

Most Popular

View all →

Discover the most used tools by our community.

Explore all tools

Browse our collection of tools

View all tools →
Share
GitHub
rootsecdev/secretsstalker

SecretsStalker

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.

View Repository
# secret_stalker

```
   ____                    __     ____  __        ____
  / __/__ ______ ___ ___  / /_   / __/ / /____ _ / / /_____ ____
 _\ \/ -_) __/ -_) -_)__/ __/  _\ \  / __/ _ `// /  '_/ -_) __/
/___/\__/\__/\__/\__/   \__/  /___/  \__/\_,_//_/_/\_\\__/_/

         ╔╦╦╬╬╬╬╬╬╦╦╗
     ╔╬╬╬╝╝┘      ╚╝╝╬╬╬┐
    ╬╬╝╚╩╬╗╔          ╚╬╬╬
   ╬╝      ╚╬╬╗╗ ╔      ╚╬╗
  ╬╬     ╔╗   ╚╬╬╬╬╬╬╦    ╬╬    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:

1. **Is it valid, and when does the secret expire?** — and if not valid, *why*
   (bad secret, expired secret, app not in the tenant…). For a valid secret it
   reads the app registration's `passwordCredentials` and reports the expiration
   date + days remaining (needs directory read; see note below).
2. **What Microsoft Graph rights does it carry?** — application permissions read
   straight from the issued token, plus **Entra directory roles** it holds (even
   detected passively from the token's `wids` claim) and **objects it owns**
   (apps/SPs you can add credentials to).
3. **What control does it have over Azure?** — RBAC role assignments at
   management-group and subscription scope.
4. **Can it reach real data?** — optional Key Vault (secrets / keys / certificates),
   Storage (blob / file / queue / table), and Cosmos DB data-plane reachability checks.
5. **What's the impact?** — dangerous permissions, roles, ownership, and reachable
   data mapped to known privesc / lateral-movement primitives, rated by severity,
   with concrete **attack-path** narratives.

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.

---

## Install

```bash
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 …`.

---

## Quickstart

The fastest way to find out what a credential can do:

```bash
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.

### Keep the secret out of your shell history

Pass credentials via environment variables instead of flags:

```bash
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.

### Authenticate with a certificate instead of a secret

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:

```bash
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`.

### Sovereign and government clouds

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:

```bash
# 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.)

---

## Recommended workflow on an engagement

1. **Refresh the permission map for this tenant (once).** Resolves Graph
   permission GUIDs to authoritative names for the target tenant:

   ```bash
   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.

2. **Passive baseline.** See validity, Graph permissions, and Azure RBAC without
   touching tenant objects:

   ```bash
   python -m secret_stalker
   ```

3. **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:

   ```bash
   python -m secret_stalker --active --export results.ndjson
   ```

---

## Reading the output

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
======================================

Credential status : VALID
Tenant            : aaaaaaaa-...
Client (app) id   : 1111...
App display name  : Recon App
SP object id      : cccc...
Secret            : valid — expires 2027-03-01 (in 207 days)

Token acquisition
=================
  OK  graph
  OK  arm
  NO  storage  — no storage token

Microsoft Graph application permissions (from token)
====================================================
  - Application.ReadWrite.All
  - Mail.Read

...

Findings — escalation / control
===============================
  [CRITICAL] (GRAPH) Application.ReadWrite.All
        Can add credentials to any app/SP and impersonate it — tenant-wide pivot.
  [CRITICAL] (ARM) Owner
        Full control including granting access to others.
  [CRITICAL] (DATA) keyvault:secrets
        Can read Key Vault secret values — connection strings, passwords, tokens.
  [MEDIUM] (GRAPH) Mail.Read
        Read all mailboxes — data exposure.

Overall risk: CRITICAL
```

- **Token acquisition** lists each audience probed (Graph, ARM, and — under
  `--active`, when matching resources are discovered — Key Vault / Storage /
  Cosmos DB). Graph and ARM are *independent*: a credential can hold one and not
  the other.
- **Secret** shows validity and, for a valid secret, the expiration date and days
  remaining (near-expiry is highlighted). See the note below on expired secrets.
- **Findings** is the part to read first — high-impact Graph permissions (`GRAPH`),
  ARM roles (`ARM`), Entra directory roles (`ROLE`), owned apps/SPs (`OWN`),
  reachable data-plane surfaces (`DATA`), and requested-but-unconsented
  consent-attack targets (`WANT`) — de-duplicated and severity-rated. Being able to
  read every Key Vault secret, or holding a directory role, is a finding on its own
  even with no dangerous Graph/ARM grant.
- **Attack paths** turns the top findings into concrete next steps (e.g. *Privileged
  Role Administrator → assign Global Administrator to self → tenant takeover*).
- **Active Graph enumeration** (`--active`) reports what each read-only probe
  returned. Most probes request a small capped page, so a filled page is shown as
  `N+` (e.g. `users  accessible  (returned 5+)`) — meaning *at least* five, not
  exactly five. Probes with no cap (`organization`, `directoryRoles`) report a
  real total with no `+`.
- **Directory roles / Owned objects / Delegated permissions** get their own sections.
  Directory roles are detected from the token's `wids` claim even without directory
  read; delegated permissions aren't usable by an app-only credential but are shown
  for user-context pivots and consent-attack targeting.
- **Overall risk** is the highest single finding's severity.

> **Secret expiration — what's knowable.** The expiry date is *not* in the token;
> it lives on the app registration's `passwordCredentials` in Entra ID. For a
> **valid** secret, secret_stalker reads it via Graph and matches your secret to
> the right credential by its `hint` (first 3 chars) — this needs directory read
> (`Application.Read.All` / `Directory.Read.All`); if the SP lacks it, the date is
> reported as unavailable rather than guessed. For an **expired** secret the auth
> itself fails, so the dead credential cannot read its own metadata — the tool
> flags it `EXPIRED (AADSTS7000222)` but the exact end date isn't retrievable
> through that credential alone.

### Exit codes

Useful for scripting:

| Code | Meaning |
|------|---------|
| `0`  | Credential is valid (got at least one token). |
| `2`  | Credential is invalid / has no access. |
| `1`  | Error — tenant could not be resolved, certificate could not be loaded, or the `--export` file could not be written. |

---

## All flags

| Flag | Effect |
|------|--------|
| `--tenant` | Tenant GUID or domain. (or `SS_TENANT`) |
| `--cloud` | Azure cloud: `public` (default), `usgov` (GCC High), `usdod` (DoD), `china` (21Vianet). Selects the Entra authority and Graph/ARM/Key Vault endpoints. Aliases like `gov`/`dod`/`commercial` accepted. (or `SS_CLOUD`) |
| `--client-id` | Application (client) ID. (or `SS_CLIENT_ID`) |
| `--secret` | Client secret. Prefer `SS_SECRET` to keep it out of history. |
| `--cert` | Certificate for JWT-assertion auth instead of a secret: a PEM (key+cert) or `.pfx`/`.p12`. Needs `cryptography`. (or `SS_CERT`) |
| `--cert-password` | Password for an encrypted `--cert` key/PFX. (or `SS_CERT_PASSWORD`) |
| `--active` | Opt-in read-only enumeration: Graph object samples **plus** Key Vault / Storage data-plane reachability. Off by default to stay quiet. |
| `--deep` | With `--active`: descend one level into reachable Storage — list blobs in accessible containers and files in accessible shares (names only, capped). Noisier. |
| `--no-arm` | Skip management-group / subscription / RBAC enumeration (Graph only). |
| `--workers N` | Parallel HTTP workers for ARM scope lookups and data-plane probes (default 8; `1` = sequential). |
| `--update-manifest` | Fetch the authoritative appRole GUID→name map from the live tenant (Graph **plus** any other resource APIs this credential is assigned on), cache it, then exit. |
| `--json` | Print the full nested result as JSON instead of the report. |
| `--export PATH` | Write results to a file. Format inferred from extension (`.csv` / `.ndjson` / `.jsonl` / `.json` / `.html`). Files are written owner-only (`0600`). |
| `--export-format` | Force the export format (`ndjson` / `csv` / `json` / `html`). |
| `--timeout N` | Per-request timeout in seconds (default 20). ARM control-plane requests (RBAC enumeration + Resource Graph discovery) use a longer timeout — `1.5×`, minimum 30s — because they run slower. |
| `--verbose`, `-v` | Trace every Graph/ARM/data-plane HTTP request (method, URL, status) to stderr. |
| `--no-banner` | Suppress the ASCII banner. |
| `--version` | Print version and exit. |

---

## Exporting results

`--export` flattens the result into **one record per discovered thing** —
credential, token, Graph permission, app-role assignment, ARM role, data-plane
hit, and scored finding — each carrying the credential context so a row stands on
its own.

```bash
# NDJSON — stream into a SIEM / log pipeline
python -m secret_stalker --export results.ndjson

# CSV — open in a spreadsheet for triage
python -m secret_stalker --active --export results.csv

# Full nested JSON to a file
python -m secret_stalker --export results.json
```

Every record carries a `record_type` (`credential`, `secret`, `token`,
`graph_permission`, `app_role_assignment`, `directory_role`, `owned_object`,
`arm_role`, `dataplane`, `delegated_permission`, `requested_permission`,
`finding`), so a consumer can filter to just what it needs — for example, the
scored hits only:

```bash
jq 'select(.record_type=="finding")' results.ndjson
```

The terminal report and `--export` work together — exporting doesn't suppress the
report (the "Exported …" confirmation goes to stderr, so piping `--json` stays
clean).

Export files carry credential context (token claims, the secret `hint`, key IDs),
so they are written **owner-only (`0600`)** to avoid leaking on a shared or synced
host. Treat them as sensitive engagement artifacts. Writing through a symlink is
refused outright, so an export path can't be redirected into truncating something
else.

Names in a result come from the tenant under assessment — app and group display
names, container and blob names — so they are treated as untrusted output:

- **CSV** values that would be read as a formula (leading `=`, `+`, `-`, `@`) are
  prefixed with a single quote, so a display name like `=cmd|' /C calc'!A0` can't
  execute when the file is opened in a spreadsheet. Spreadsheets strip the quote
  on display.
- **Terminal, CSV and HTML** output has control characters stripped, so a name
  carrying ANSI escapes can't retitle your terminal or overwrite the findings
  above it — whether you read the report live, `cat` the CSV, or `cat` the HTML.
- **JSON / NDJSON are left faithful**: `json.dumps` encodes control characters as
  `\uXXXX`, which is inert as text while a parser still round-trips the exact
  value the tenant returned. The raw name is evidence, so it is preserved there.

---

## How permission GUIDs are resolved

`appRoleAssignments` come back as GUIDs. secret_stalker resolves them to names by
a flat lookup (appRole GUIDs are globally unique), which keeps working **even when
directory reads are denied**:

- A best-effort map of well-known Graph permissions ships in
  `secret_stalker/data/graph_app_roles.json`.
- `--update-manifest` overrides it with authoritative data pulled live from the
  in-scope tenant — Microsoft Graph **plus every other resource API this credential
  is assigned on** (e.g. Exchange Online, SharePoint), so non-Graph GUIDs resolve too.
- An unknown GUID is shown **raw and flagged** — the tool never guesses a name.

---

## How it works (the short version)

- **Validity + permissions in one request.** A successful Graph token's `roles`
  claim *is* the list of granted application permissions. secret_stalker reads it
  from the decoded token — fast and quiet, no Graph calls needed.
- **Graph ≠ ARM.** They are different token audiences. A credential can hold rights
  on one and not the other, so each is probed independently.
- **Data plane ≠ control plane.** Having ARM rights over a Key Vault (management)
  is not the same as being able to read its secrets (data plane). Under `--active`,
  data-plane reachability is tested with the resource's own token audience — and it
  lists object **names only**, never values or contents.
- **Per-surface data-plane probing.** Data-plane RBAC is granted per object type /
  service, so each is probed independently: Key Vault **secrets / keys /
  certificates**, Storage **blob / file / queue / table**, and Cosmos DB
  **databases**. A credential that's `Storage File Data SMB Share Reader` but not a
  blob reader is surfaced, not missed. (Cosmos uses a non-standard AAD REST header
  and is **best-effort** — validate a `denied` result against a live account.)
- **Tenant-wide discovery.** Resources are found with a single Azure Resource Graph
  sweep across every subscription the principal can see (RBAC-respecting), falling
  back to per-subscription provider listing if ARG is denied. The report tags which
  path was used (`[discovery: resource-graph]` vs `per-subscription`). The sweep
  pages through results up to a bound (40 pages × 1000 rows per resource type) so a
  run always terminates; `--verbose` says so if the bound is ever reached.
- **Severity mapping** lives in `secret_stalker/risk.py` — edit it to tune what
  your team treats as high-impact.

---

## Project layout

```
secret_stalker/
  clouds.py      Azure cloud endpoint table (public / usgov / usdod / china)
  auth.py        client_credentials flow + tenant discovery + AADSTS decoding
  jwt_utils.py   local JWT claim extraction
  manifest.py    Graph appRole GUID -> name resolution (bundled + live cache)
  graph.py       service principal lookup + appRole resolution + active probes
  arm.py         management-group / subscription RBAC + resource discovery
  dataplane.py   Key Vault / Storage data-plane reachability probes
  risk.py        permission/role/data-plane -> impact mapping (tune this)
  report.py      terminal + JSON output
  export.py      flatten to NDJSON / CSV / JSON records for ingestion
  util.py        shared HTTP (retry/backoff + safe JSON), pmap parallel map,
                 untrusted-output sanitizing
  banner.py      ASCII banner (stderr only)
  cli.py         orchestration
  data/graph_app_roles.json   bundled permission manifest
tests/           pytest suite (run: pytest)
pyproject.toml   packaging + `secret_stalker` console entry point
```

---

## Implemented highlights

- **Secret or certificate auth** (`--cert`) — JWT client-assertion (RS256) from a
  PEM or PFX, with certificate-expiry reporting. `auth.py`
- **Entra directory roles** held by the principal — including passive detection
  from the token's `wids` claim (no directory read needed) — scored by role. `graph.py` / `risk.py`
- **Owned objects** — apps/SPs the credential owns (a credential-add pivot). `graph.py`
- **Delegated-permission visibility** (`--active`) — consented grants + requested
  permissions, with unconsented dangerous perms flagged as consent-attack targets. `graph.py` / `risk.py`
- **Attack-path narratives** + **HTML report** (`--export report.html`). `risk.py` / `report.py`
- **Commercial + sovereign clouds** (`--cloud`) — public, US Gov (GCC High), US DoD,
  and China (21Vianet), each with the correct Entra authority and Graph / ARM / Key
  Vault audiences. `clouds.py`
- **Tenant-wide resource discovery** via a single Azure Resource Graph sweep
  (RBAC-respecting), with per-subscription fallback and management-group descendant
  recovery for subscriptions the flat list omits. `arm.py`
- **Full data-plane coverage** — Key Vault secrets / keys / certificates, Storage
  blob / file / queue / table, and Cosmos DB databases, each probed independently
  and folded into the scored findings (reachable data is a finding, not just
  metadata). `dataplane.py` / `risk.py`
- **Accurate counts** — data-plane listings follow continuation tokens (bounded), so
  a surface count reflects the real total; a `+` (e.g. `25+`) marks where the listing
  was capped rather than silently under-reporting. `dataplane.py`
- **Throttling-resilient requests** — one shared HTTP path retries `429`/`503`
  honoring `Retry-After`, so transient throttling isn't misread as "denied / no
  access." `util.py`
- **Deep Storage descent** (`--deep`) — lists blobs in reachable containers and
  files in reachable shares, names only and capped. `dataplane.py`
- **Non-Graph GUID resolution** — `--update-manifest` caches appRoles for every
  resource API the credential is assigned on, not just Graph. `graph.py` / `manifest.py`
- **Concurrency** (`--workers N`) across ARM scope lookups and data-plane probes,
  with per-item error isolation. `util.py`

## Roadmap ideas

- Key Vault keys/certificates *attribute* read (enabled/expiry) for triage —
  still metadata only, no key material.
- Storage queue/table content peeking would cross into reading data, so it is
  intentionally **out of scope** for this read-only tool.
Download Tool