Skip to main content

azath — Azure Attack-Path Analyzer

CI Python 3.11+ License: MIT

The kind of identity attack graph BloodHound builds — with the analysis it leaves to you.

azath reads an Azure / Entra tenant read-only, builds a typed graph of identities, roles, and resources, finds privilege-escalation paths under an assumed-breach model, and uses an LLM to explain and prioritize them in plain English.

ℹ️ Independent tool. azath builds its own graph directly from Microsoft Graph and Azure Resource Manager. It does not use, wrap, import from, or depend on BloodHound or AzureHound — they're named here only to place this tool in a landscape readers already know.

azath scan
┏━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Severity ┃ Finding                                              ┃ ID                    ┃
┡━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━┩
│ CRITICAL │ Dana (contractor) can become 'build-pipeline-sp' and │ AZATH-CRED-8D676C70C5 │
│          │ reach Owner at subscription 0000…00ff                │                       │
│ CRITICAL │ Helpdesk Tier 1 can reach Owner at subscription      │ AZATH-GRP-E6E22CFAA8  │
│          │ 0000…00ff via group 'Azure Subscription Admins'      │                       │
│ HIGH     │ Compromise of 'lab-ingest-func' reaches Owner at     │ AZATH-MI-A64C9FA465   │
│          │ resource group 'rg-lab'                              │                       │
└──────────┴──────────────────────────────────────────────────────┴───────────────────────┘

Each finding carries the full path and the evidence behind every hop:

1. Helpdesk Tier 1        --[member_of]-->    Helpdesk Operators
2. Helpdesk Operators     --[member_of]-->    Azure Subscription Admins
3. Azure Subscription Admins --[has_role]-->  Owner
4. Owner              --[assigned_at]-->      subscription 0000…00ff

A helpdesk-tier user holding subscription Owner through two layers of group nesting — invisible to anyone reading the subscription's access list, because their name isn't on it.

📄 Sample report · 🕸️ Interactive graph demo — both generated from synthetic fixtures, so you can see real output without an Azure account.


The design thesis

The LLM is a reader and synthesizer of structured findings, never an actor.

Deterministic code gathers and structures the data; the LLM explains and prioritizes it. It never queries Azure, never decides what to collect, and cannot invent a finding — every claim traces back to typed evidence produced by static analysis. If the LLM is unavailable, the report still renders in full.

Why another tool?

Tool class Examples Strength The gap azath fills
Config-checklist auditors ScoutSuite, Prowler Broad CIS/CSPM coverage Grade config, not reachability — they miss multi-hop paths
Raw graph tools BloodHound, AzureHound Powerful identity graphs Require an expert to read; no prioritization or narrative

azath isn't trying to out-breadth BloodHound or out-checklist Prowler. Its one job is the "so what, and what do I fix first" that a raw graph or a checklist leaves to you.

Threat model: assumed breach

It doesn't ask "does this config match a benchmark?" It asks:

"If an attacker already controls identity X, where can they get to, and how?"

Every finding is a concrete path from a starting principal to elevated privilege, with the exact roles and permissions that make each hop possible.

What it detects

# Path Hops Why it's easy to miss
1 roleAssignments/write self-escalation — Owner / User Access Administrator 2 Looks like normal admin access until you notice it's self-granting
2 Group-mediated — members through nested groups, and group owners 4+ The user's name never appears on the subscription's access list
3 Managed-identity abuse — a VM/Function/Automation Account whose identity is privileged 3 Code execution on the resource is authentication as the identity
4 App-credential addition — app ownership, Application.ReadWrite.All, or a credential-capable directory role 3 Runs through Entra, so an Azure access review structurally cannot see it
5 Hidden dangerous actionsrunCommand, listKeys, Key Vault policy writes 2 Nobody reads a 40-entry actions array; they read "Storage Operations Helper"

Severity tracks blast radius: the same role is CRITICAL at subscription root, HIGH on a resource group, MEDIUM on a single resource.

Setup

Requires Python 3.11+ and a read-only service principal. The full app-registration walkthrough — with why at each step — is in docs/AZURE_SETUP.md.

git clone https://github.com/tomerblum/azath
cd azath

python3.11 -m venv .venv && source .venv/bin/activate
python --version                          # confirm 3.11+ before continuing
pip install --upgrade pip                 # editable installs need pip >= 21.3
pip install -e ".[dev]"

cp .env.example .env && chmod 600 .env   # then fill in your values
azath config-check                        # confirm what's set
azath smoke-test                          # THE GATE: must pass before anything else
azath scan                                # -> reports/attack-paths.md

macOS note: python3 is usually Apple's system Python 3.9 even after brew install python@3.11 — Homebrew's python@3.11 is keg-only. Build the venv with python3.11 explicitly, or you'll hit ERROR: File "setup.py" or "setup.cfg" not found (system pip 21.2.4 predates PEP 660 editable installs).

Commands

Command Purpose
azath smoke-test Verify auth against both Microsoft Graph and Azure Resource Graph
azath scan Collect → detect → explain → Markdown report
azath scan --no-llm Deterministic findings only, no LLM call
azath scan --json out.json Also export findings + snapshot as JSON
azath diff old.json new.json What's new, resolved, or re-scored since a previous scan
azath graph -o graph.html Interactive Cytoscape graph, attack paths highlighted
azath graph -f dot -o g.dot Graphviz export — dot -Tsvg g.dot -o g.svg
azath models List models your LLM endpoint accepts
azath config-check Show which settings are present (secrets masked)

Tracking change over time

azath scan --json baseline.json
# ...a week later...
azath scan --json current.json
azath diff baseline.json current.json -o diff.md

Findings are matched by ID — a stable hash of the principal, role, and scope that define the path, not of anything incidental like collection order. So an unchanged tenant produces an empty diff, and a real change shows up as exactly one new or resolved finding.

Seeing the graph

azath graph -o graph.html                    # scans live, then renders
azath graph --from scan.json -o graph.html   # or render a saved export
azath graph -f dot -o g.dot                  # Graphviz, no browser needed

Nodes are identities, roles, scopes, and resources; anything on a detected attack path is coloured by the worst severity of any path it appears on. The page renders the same graph the detectors traverse, so the picture and the findings can never disagree.

Layout defaults to layered left-to-right, because an attack path flows — actor, then the relationship carrying privilege, then the privilege, then its scope. A force-directed layout hides exactly that.

📊 Try it: live interactive demo — click a finding to isolate its path, or drag nodes around. Source: docs/sample-graph.html (GitHub shows the markup rather than rendering it) and a Graphviz version that needs no browser at all.

How it works

deterministic collectors  ->  typed Pydantic models  ->  graph + path-finder
    ->  provider-agnostic LLM synthesis  ->  Markdown report  (+ JSON export)
  • Collectors gather one slice each via Microsoft Graph and ARM, returning typed models. They handle paging, throttling, and partial failure. Expansion is seeded from privilege-bearing objects, so cost scales with the interesting subgraph rather than tenant size.
  • Graph is networkx. Nodes: identities, roles, scopes, resources. Edges: has_role, assigned_at, member_of, owner_of, has_managed_identity, can_add_credential_to.
  • Detectors traverse the graph deterministically, one per path type, each emitting a typed AttackPath with severity and traceable evidence.
  • LLM synthesis turns those findings into the narrative. Provider-agnostic: an OpenAI-compatible client (default, targets OpenRouter) and a native Anthropic client.

Decisions worth knowing about

NotActions is the correctness crux. Contributor has Actions: ["*"] — which matches roleAssignments/write — but excludes Microsoft.Authorization/*/Write. Naive matching on Actions alone would flag every Contributor in every tenant. Owner has the identical ["*"] with no such exclusion, so it must be flagged. Both are pinned by tests.

Groups and managed identities aren't reported as actors. Nobody compromises a group. When a group holds a privileged role, the finding is attributed to the members and owners who can actually wield it. Service principals are reported directly — you can steal their existing secret — so credential-addition findings sit alongside them rather than replacing them.

Fail loud, not quiet. When a detector can't complete an attribution — a privileged group whose membership won't enumerate, a managed identity whose host isn't in scope — it says so as a finding. An unreadable privileged group is scored by scope breadth, not capped at medium, because unknown exposure on subscription Owner is not a medium problem.

Data-access model

Read-only service principal, OAuth client-credentials flow:

  • Microsoft Graph Directory.Read.All — users, groups, service principals, app registrations, directory roles.
  • Azure RBAC Reader at subscription scope — role assignments, role definitions, resources.

No write permission of any kind is used or requested.

Security & authorized use

The service principal's client secret grants Directory.Read.All — effectively keys to read the entire directory. Treat it accordingly:

  • The secret lives only in a gitignored .env; chmod 600 .env.
  • Rotate it periodically; grant least privilege (Reader, not more).
  • Point this only at tenants you own or are explicitly authorized to assess.
  • Never point it at a production or employer tenant. Use a free personal Azure account seeded with a deliberately-vulnerable lab (see the setup doc).

Development

pytest          # no live Azure calls — fixtures only
mypy            # --strict, clean
ruff check .

Python 3.11+, src/ layout, Typer CLI, Pydantic v2. Tests run against captured/synthetic Azure JSON; nothing touches a live tenant, so CI needs no credentials. CI additionally regenerates the committed sample outputs and fails if they drift, so the examples in this repo always match the code.

Security policy and authorized-use expectations: SECURITY.md.

Limitations

  • Detects the five path types above. Absence of findings is not proof of absence of attack paths.
  • Point-in-time snapshot of a single subscription — no management-group traversal, PIM, or Conditional Access awareness.
  • Group expansion covers groups holding role assignments and anything nested inside them; unprivileged groups aren't enumerated.
  • Managed-identity hosts resolve only within the scanned subscription.
  • The dangerous-action catalogue is curated, not exhaustive.
  • Not a replacement for BloodHound's breadth or a CSPM's checklist coverage — complementary and deliberately focused.

Roadmap

Management-group traversal and cross-subscription paths, PIM and Conditional Access awareness, and scoring paths by exploitability as well as blast radius.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

azath-0.1.0.tar.gz (89.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

azath-0.1.0-py3-none-any.whl (74.5 kB view details)

Uploaded Python 3

File details

Details for the file azath-0.1.0.tar.gz.

File metadata

  • Download URL: azath-0.1.0.tar.gz
  • Upload date:
  • Size: 89.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for azath-0.1.0.tar.gz
Algorithm Hash digest
SHA256 2b37b5dde47caea51b491dc2d1795b050bfd1311175a34aea7cc069298306100
MD5 43f20dcfffda6b745ff5734189ac303f
BLAKE2b-256 256beb2174a6b1b3aba4e6b963eed29a2a3c9584dd964f00fdc9d453cfbbcbbd

See more details on using hashes here.

Provenance

The following attestation bundles were made for azath-0.1.0.tar.gz:

Publisher: publish.yml on tomerblum/azath

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file azath-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: azath-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 74.5 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for azath-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 632ebb5b9e835b38a18a05963ad6ad9e51208c658631cf98bceb53b1209d7fc3
MD5 2d53033ccca8fabe959d255566f59cb1
BLAKE2b-256 b5e8442621cd319e5b3f2214b7607184a40250edda912bb98f587f7a350af593

See more details on using hashes here.

Provenance

The following attestation bundles were made for azath-0.1.0-py3-none-any.whl:

Publisher: publish.yml on tomerblum/azath

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.1.1

2 files

This release

0.1.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page