Metadata-Version: 2.5
Name: azath
Version: 0.1.1
Summary: Assumed-breach Azure/Entra attack-path analyzer: deterministic collectors build a typed identity graph, detectors find privilege-escalation paths, an LLM explains and prioritizes them.
Project-URL: Repository, https://github.com/tomerblum/azath
Project-URL: Issues, https://github.com/tomerblum/azath/issues
Author: Tomer Blum
License: MIT
License-File: LICENSE
Keywords: attack-path,azure,cspm,entra,privilege-escalation,security
Classifier: Development Status :: 3 - Alpha
Classifier: Environment :: Console
Classifier: Intended Audience :: Information Technology
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.11
Classifier: Topic :: Security
Requires-Python: >=3.11
Requires-Dist: azure-identity>=1.17
Requires-Dist: azure-mgmt-authorization>=4.0
Requires-Dist: azure-mgmt-resourcegraph>=8.0
Requires-Dist: httpx>=0.27
Requires-Dist: networkx>=3.3
Requires-Dist: pydantic-settings>=2.3
Requires-Dist: pydantic>=2.7
Requires-Dist: rich>=13.7
Requires-Dist: typer>=0.12
Provides-Extra: dev
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: pytest-cov>=5.0; extra == 'dev'
Requires-Dist: pytest>=8.2; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# azath - Azure Attack-Path Analyzer

[![PyPI](https://img.shields.io/pypi/v/azath)](https://pypi.org/project/azath/)
[![CI](https://github.com/tomerblum/azath/actions/workflows/ci.yml/badge.svg)](https://github.com/tomerblum/azath/actions/workflows/ci.yml)
[![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/license-MIT-green)](https://github.com/tomerblum/azath/blob/main/LICENSE)

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

```bash
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](https://github.com/tomerblum/azath/blob/main/docs/sample-report.md)** · 🕸️ **[Interactive graph
demo](https://tomerblum.github.io/azath/sample-graph.html)** — 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 actions** — `runCommand`, `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.

## Install

```bash
pip install azath
```

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](https://github.com/tomerblum/azath/blob/main/docs/AZURE_SETUP.md)**.

```bash
curl -O https://raw.githubusercontent.com/tomerblum/azath/main/.env.example
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
```

### From source

```bash
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 smoke-test
```

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

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

```bash
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](https://tomerblum.github.io/azath/sample-graph.html) — click a finding to
isolate its path, or drag nodes around. Source:
[`docs/sample-graph.html`](https://github.com/tomerblum/azath/blob/main/docs/sample-graph.html)
(GitHub shows the markup rather than rendering it) and a
[Graphviz version](https://github.com/tomerblum/azath/blob/main/docs/sample-graph.dot)
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

```bash
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](https://github.com/tomerblum/azath/blob/main/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](https://github.com/tomerblum/azath/blob/main/LICENSE).
