
Valutazione in sola lettura delle credenziali delle app di Entra ID: enumera le autorizzazioni di Graph, l'RBAC di Azure e i dati cloud raggiungibili, quindi mappa i risultati sui percorsi di privilege escalation e lateral movement.
/ / ______ ___ ___ / /_ / / / /__ _ / / /_____ ____ \ / -) / -) -)/ / \ \ / __/ _ `// / '/ -) / //_/_/_/_/ _/ // _/_,////_\__/_/
╔╦╦╬╬╬╬╬╬╦╦╗
╔╬╬╬╝╝┘ ╚╝╝╬╬╬┐
╬╬╝╚╩╬╗╔ ╚╬╬╬
╬╝ ╚╬╬╗╗ ╔ ╚╬╗ ╬╬ ╔╗ ╚╬╬╬╬╬╬╦ ╬╬ 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. \
**Cosa può realmente fare questo client ID + secret di Entra ID?**
Hai trovato una credenziale applicativa di Entra ID (Azure AD) — un client ID e un secret —
durante un impegno autorizzato, e il tenant a cui appartiene è in scope.
`secret_stalker` prende questi due valori e ti dice, a partire da zero:
1. **È valido e quando scade il secret?** — e se non è valido, *perché*
(secret errato, secret scaduto, app non presente nel tenant…). Per un secret valido
legge le `passwordCredentials` della registrazione dell'app e riporta la data
di scadenza + i giorni rimanenti (richiede la lettura della directory; vedi nota sotto).
2. **Quali diritti Microsoft Graph porta con sé?** — autorizzazioni applicative lette
direttamente dal token emesso, più **ruoli di directory Entra** che detiene (anche
rilevati passivamente dalla dichiarazione `wids` del token) e **oggetti che possiede**
(app/SP a cui puoi aggiungere credenziali).
3. **Che controllo ha su Azure?** — assegnazioni di ruolo RBAC a livello di
gruppo di gestione e sottoscrizione.
4. **Può raggiungere dati reali?** — controlli opzionali di raggiungibilità del data-plane
per Key Vault (segreti / chiavi / certificati), Storage (blob / file / coda / tabella)
e Cosmos DB.
5. **Qual è l'impatto?** — permessi pericolosi, ruoli, proprietà e dati raggiungibili
mappati su primitive note di privesc / movimento laterale, valutati per gravità,
con narrazioni concrete di **attack-path**.
Si autentica con un **client secret** o un **certificato** (`--cert`),
e funziona con **cloud commerciali e sovrani** (`--cloud`).
È **passivo per impostazione predefinita** e **non modifica mai nulla** — solo
enumerazione in sola lettura.
> ⚠️ **Solo test autorizzati.** Eseguilo esclusivamente contro tenant che sono
> esplicitamente in scope per un impegno che sei autorizzato a svolgere.
---
## Installazione```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
L'unica dipendenza runtime è requests. I token vengono decodificati localmente (base64 + JSON) — nessuna verifica della firma, nessuna libreria crittografica, nessun SDK Microsoft. L'unica eccezione è l'autenticazione tramite certificato (--cert), che richiede il pacchetto opzionale cryptography per firmare l'asserzione client JWT. Richiede Python 3.7+.
Dopo pip install . puoi invocarlo come secret_stalker … invece di python -m secret_stalker ….
Il modo più veloce per scoprire cosa può fare una credenziale:```bash
python -m secret_stalker
--tenant contoso.onmicrosoft.com
--client-id 11111111-2222-3333-4444-555555555555
--secret ''
`--tenant` accetta un GUID tenant o un dominio — un dominio viene risolto automaticamente nel relativo ID tenant tramite l'endpoint pubblico di configurazione OpenID.
### Tieni il segreto fuori dalla cronologia della shell
Passa le credenziali tramite variabili d'ambiente invece che tramite flag:```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
Ognuno di --tenant / --client-id / --secret può provenire da SS_TENANT /
SS_CLIENT_ID / SS_SECRET. I flag hanno precedenza sull'ambiente.
Non si tratta solo della cronologia della shell: un valore argv è leggibile da qualsiasi utente
locale per l'intera durata del processo (ps, /proc/<pid>/cmdline). Se --secret
o --cert-password viene passato come flag, lo strumento stampa un promemoria di una riga su
stderr — non compare mai nell'output di --json o --export.
Le registrazioni delle app spesso usano un certificato anziché un segreto. Passa --cert
(un PEM contenente la chiave privata e il certificato, o un .pfx/.p12) e lo
strumento si autentica con un'asserzione client JWT firmata:```bash
python -m secret_stalker --tenant contoso.onmicrosoft.com
--client-id --cert ./app.pem # or app.pfx
python -m secret_stalker ... --cert app.pfx --cert-password ''
L'autenticazione tramite certificato richiede il pacchetto opzionale `cryptography` (`pip install '.[cert]'`).
Lo strumento riporta la scadenza del certificato stesso (identificata in base al relativo thumbprint presente in
`keyCredentials` dell'app), esattamente come fa per un segreto. `--cert`/`--cert-password`
vengono letti anche da `SS_CERT` / `SS_CERT_PASSWORD`.
### Cloud sovrani e governativi
Per impostazione predefinita secret_stalker si rivolge al cloud **commerciale**. Per i tenant sovrani,
passa `--cloud` (o `SS_CLOUD`) affinché l'autorità Entra e gli endpoint Graph / ARM / Key
Vault corrispondano — altrimenti credenziali valide sembrano non avere accesso:```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 ...
Sono accettati alias come gov, dod, commercial, gcc-high e 21vianet.
(L'audience del data-plane di Storage, storage.azure.com, è la stessa in ogni cloud.)
Questo autentica, memorizza nella cache la mappa delle appRole Graph del tenant in
~/.secret_stalker/app_roles_cache.json, e esce. Salta questo passaggio se le
credenziali non possono leggere i service principal — la mappa inclusa copre comunque le
autorizzazioni note.
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)
OK graph OK arm NO storage — no storage token
...
[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** elenca ogni audience sondato (Graph, ARM e — con
`--active`, quando vengono scoperte risorse corrispondenti — Key Vault / Storage /
Cosmos DB). Graph e ARM sono *indipendenti*: una credenziale può avere l'uno senza
l'altro.
- **Secret** mostra la validità e, per un secret valido, la data di scadenza e i giorni
rimanenti (la vicinanza alla scadenza è evidenziata). Vedi la nota sotto sui secret scaduti.
- **Findings** è la sezione da leggere per prima — permessi Graph ad alto impatto (`GRAPH`),
ruoli ARM (`ARM`), ruoli di directory Entra (`ROLE`), app/SP posseduti (`OWN`),
superfici data-plane raggiungibili (`DATA`) e obiettivi di attacco di consenso
richiesti ma non autorizzati (`WANT`) — deduplicati e classificati per gravità. Poter
leggere ogni secret di Key Vault, o detenere un ruolo di directory, è di per sé un finding
anche senza concessioni Graph/ARM pericolose.
- **Percorsi di attacco** trasforma i finding principali in passi successivi concreti (es. *Privileged
Role Administrator → assegnare Global Administrator a sé stessi → tenant takeover*).
- **Enumerazione Graph attiva** (`--active`) riporta ciò che ogni sonda di sola lettura ha
restituito. La maggior parte delle sonde richiede una piccola pagina con limite, quindi una
pagina piena viene mostrata come `N+` (es. `users accessible (returned 5+)`) — ovvero
*almeno* cinque, non esattamente cinque. Le sonde senza limite (`organization`,
`directoryRoles`) riportano un totale reale senza `+`.
- **Ruoli di directory / Oggetti posseduti / Permessi delegati** hanno sezioni dedicate.
I ruoli di directory vengono rilevati dal claim `wids` del token anche senza lettura della
directory; i permessi delegati non sono utilizzabili da una credenziale solo-app ma vengono
mostrati per pivot nel contesto utente e per il targeting di attacchi di consenso.
- **Rischio complessivo** è la gravità del singolo finding di livello più alto.
> **Scadenza del secret — cosa è conoscibile.** La data di scadenza *non* è nel token;
> risiede nei `passwordCredentials` della registrazione dell'app in Entra ID. Per un
> secret **valido**, secret_stalker la legge tramite Graph e abbina il tuo secret alla
> credenziale giusta tramite il suo `hint` (primi 3 caratteri) — questo richiede la lettura
> della directory (`Application.Read.All` / `Directory.Read.All`); se l'SP non la possiede, la
> data viene riportata come non disponibile piuttosto che ipotizzata. Per un secret **scaduto**
> l'autenticazione stessa fallisce, quindi la credenziale morta non può leggere i propri
> metadati — lo strumento lo segnala come `EXPIRED (AADSTS7000222)` ma la data di fine esatta
> non è recuperabile attraverso quella sola credenziale.
### Codici di uscita
Utili per lo scripting:
| Codice | Significato |
|------|---------|
| `0` | La credenziale è valida (ottenuto almeno un token). |
| `2` | La credenziale non è valida / non ha accesso. |
| `1` | Errore — il tenant non ha potuto essere risolto, il certificato non ha potuto essere caricato, o il file `--export` non ha potuto essere scritto. |
---
## Tutti i flag
| Flag | Effetto |
|------|--------|
| `--tenant` | GUID o dominio del tenant. (o `SS_TENANT`) |
| `--cloud` | Cloud Azure: `public` (predefinito), `usgov` (GCC High), `usdod` (DoD), `china` (21Vianet). Seleziona l'autorità Entra e gli endpoint Graph/ARM/Key Vault. Accettati alias come `gov`/`dod`/`commercial`. (o `SS_CLOUD`) |
| `--client-id` | ID applicazione (client). (o `SS_CLIENT_ID`) |
| `--secret` | Client secret. Preferisci `SS_SECRET` per tenerlo fuori dalla cronologia. |
| `--cert` | Certificato per l'autenticazione JWT-assertion al posto di un secret: un PEM (key+cert) o `.pfx`/`.p12`. Richiede `cryptography`. (o `SS_CERT`) |
| `--cert-password` | Password per una chiave/PFX `--cert` criptata. (o `SS_CERT_PASSWORD`) |
| `--active` | Enumerazione di sola lettura opt-in: campioni di oggetti Graph **più** raggiungibilità data-plane di Key Vault / Storage. Disattivata di default per rimanere silenziosi. |
| `--deep` | Con `--active`: scendi di un livello nello Storage raggiungibile — elenca i blob nei contenitori accessibili e i file nelle condivisioni accessibili (solo nomi, con limite). Più rumoroso. |
| `--no-arm` | Salta l'enumerazione management-group / subscription / RBAC (solo Graph). |
| `--workers N` | Worker HTTP paralleli per le ricerche di scope ARM e le sonde data-plane (predefinito 8; `1` = sequenziale). |
| `--update-manifest` | Recupera la mappa autorevole GUID→nome degli appRole dal tenant live (Graph **più** qualsiasi altra API risorsa su cui questa credenziale è assegnata), la mette in cache, poi esce. |
| `--json` | Stampa l'intero risultato nidificato come JSON invece del report. |
| `--export PATH` | Scrivi i risultati su un file. Formato dedotto dall'estensione (`.csv` / `.ndjson` / `.jsonl` / `.json` / `.html`). I file vengono scritti solo per il proprietario (`0600`). |
| `--export-format` | Forza il formato di esportazione (`ndjson` / `csv` / `json` / `html`). |
| `--timeout N` | Timeout per richiesta in secondi (predefinito 20). Le richieste al control-plane ARM (enumerazione RBAC + discovery di Resource Graph) usano un timeout più lungo — `1.5×`, minimo 30s — perché sono più lente. |
| `--verbose`, `-v` | Traccia ogni richiesta HTTP Graph/ARM/data-plane (metodo, URL, stato) su stderr. |
| `--no-banner` | Sopprimi il banner ASCII. |
| `--version` | Stampa la versione ed esci. |
---
## Esportazione dei risultati
`--export` appiattisce il risultato in **un record per ogni elemento scoperto** —
credenziale, token, permesso Graph, assegnazione app-role, ruolo ARM, hit
data-plane e finding con punteggio — ciascuno con il contesto della credenziale,
così che ogni riga sia autosufficiente.```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
Ogni record contiene un record_type (credential, secret, token,
graph_permission, app_role_assignment, directory_role, owned_object,
arm_role, dataplane, delegated_permission, requested_permission,
finding), così un consumatore può filtrare solo ciò di cui ha bisogno — ad esempio, solo i
risultati con punteggio:```bash
jq 'select(.record_type=="finding")' results.ndjson
Il report sul terminale e `--export` lavorano insieme: l'esportazione non sopprime il report (la conferma "Esportato …" va su stderr, quindi reindirizzare `--json` tramite pipe resta pulito).
I file di esportazione includono il contesto delle credenziali (claims del token, `hint` del segreto, ID delle chiavi), quindi vengono scritti **solo proprietario (`0600`)** per evitare fughe su un host condiviso o sincronizzato. Trattali come artefatti sensibili dell'engagement. La scrittura attraverso un symlink viene rifiutata direttamente, quindi un percorso di esportazione non può essere reindirizzato per troncare altro.
I nomi in un risultato provengono dal tenant oggetto di valutazione — nomi visualizzati di app e gruppi, nomi di container e blob — pertanto vengono trattati come output non fidato:
- I valori **CSV** che verrebbero letti come formula (`=`, `+`, `-`, `@` iniziali) vengono preceduti da un apice singolo, così un nome visualizzato come `=cmd|' /C calc'!A0` non può essere eseguito quando il file viene aperto in un foglio di calcolo. I fogli di calcolo rimuovono l'apice nella visualizzazione.
- L'output **Terminale, CSV e HTML** ha i caratteri di controllo rimossi, quindi un nome che trasporta sequenze ANSI non può riscrivere il titolo del tuo terminale né sovrascrivere i risultati sopra di esso — sia che tu legga il report dal vivo, esegua `cat` sul CSV o `cat` sull'HTML.
- **JSON / NDJSON restano fedeli**: `json.dumps` codifica i caratteri di controllo come `\uXXXX`, che sono inerti come testo mentre un parser ricostruisce comunque il valore esatto restituito dal tenant. Il nome grezzo è una prova, quindi viene conservato lì.
---
## Come vengono risolti i GUID dei permessi
`appRoleAssignments` arrivano come GUID. secret_stalker li risolve in nomi tramite una lookup piatta (i GUID di appRole sono globalmente unici), che continua a funzionare **anche quando le letture della directory sono negate**:
- Una mappa best-effort delle permission Graph note è inclusa in `secret_stalker/data/graph_app_roles.json`.
- `--update-manifest` la sovrascrive con dati autorevoli recuperati dal vivo dal tenant in scope — Microsoft Graph **più ogni altra API di risorsa su cui questa credenziale è assegnata** (es. Exchange Online, SharePoint), quindi anche i GUID non Graph vengono risolti.
- Un GUID sconosciuto viene mostrato **grezzo e segnalato** — lo strumento non indovina mai un nome.
---
## Come funziona (in breve)
- **Validità + permessi in una sola richiesta.** La claim `roles` di un token Graph valido *è* l'elenco dei permessi applicazione concessi. secret_stalker la legge dal token decodificato — veloce e silenzioso, nessuna chiamata Graph necessaria.
- **Graph ≠ ARM.** Sono audience token diverse. Una credenziale può avere diritti sull'una e non sull'altra, quindi ciascuna viene sondata in modo indipendente.
- **Data plane ≠ control plane.** Avere diritti ARM su un Key Vault (gestione) non equivale a poter leggere i suoi segreti (data plane). Con `--active`, la raggiungibilità del data plane viene testata con l'audience token della risorsa stessa — ed elenca **solo i nomi** degli oggetti, mai valori o contenuti.
- **Sondaggio del data plane per superficie.** La RBAC del data plane è concessa per tipo di oggetto / servizio, quindi ciascuno viene sondato in modo indipendente: Key Vault **secrets / keys / certificates**, Storage **blob / file / queue / table** e Cosmos DB **databases**. Una credenziale che possiede `Storage File Data SMB Share Reader` ma non è in grado di leggere blob viene evidenziata, non ignorata. (Cosmos usa un header REST AAD non standard ed è **best-effort** — valida un risultato `denied` con un account reale.)
- **Scoperta a livello di tenant.** Le risorse vengono trovate con una singola scansione di Azure Resource Graph attraverso tutte le subscription che il principal può vedere (rispettando la RBAC), con fallback all'elenco dei provider per subscription se ARG viene negato. Il report indica quale percorso è stato usato (`[discovery: resource-graph]` vs `per-subscription`). La scansione pagina i risultati fino a un limite (40 pagine × 1000 righe per tipo di risorsa), quindi un'esecuzione termina sempre; `--verbose` lo segnala se il limite viene mai raggiunto.
- **Mappatura della severità** si trova in `secret_stalker/risk.py` — modificala per regolare ciò che il tuo team considera ad alto impatto.
---
## Struttura del progetto```
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
--cert) — JWT client-assertion (RS256) da
PEM o PFX, con segnalazione della scadenza del certificato. auth.pywids del token (senza bisogno di letture in directory) — valutati per ruolo. graph.py / risk.pygraph.py--active) — concessioni consentite + permessi
richiesti, con i permessi pericolosi non consentiti segnalati come bersagli per attacchi di consenso. graph.py / risk.py--export report.html). risk.py / report.py--cloud | Entra authority | Microsoft Graph | ARM | Key Vault |
|---|
public (predefinito) | 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 |
--cloud) — pubblico, US Gov (GCC High), US DoD,
e Cina (21Vianet), ciascuno con la corretta autorità Entra e i destinatari Graph / ARM / Key
Vault. clouds.pyarm.pydataplane.py / risk.py+ (es. 25+) segnala i casi in cui l'elenco
è stato limitato anziché riportare in modo silenzioso un valore inferiore. dataplane.py429/503
rispettando Retry-After, così una limitazione transitoria non viene interpretata come "negato / nessun
accesso." util.py--deep) — elenca i blob nei contenitori raggiungibili e i
file nelle condivisioni raggiungibili, solo nomi e con limite massimo. dataplane.py--update-manifest memorizza nella cache gli appRoles per ogni
API della risorsa su cui la credenziale è assegnata, non solo Graph. graph.py / manifest.py--workers N) nelle ricerche degli ambiti ARM e nei sondaggi del data-plane,
con isolamento degli errori per singolo elemento. util.py