An IAM principal rarely holds AdministratorAccess directly. It holds iam:PassRole and lambda:CreateFunction. Or iam:CreatePolicyVersion on a policy attached to itself. Or it can assume a role whose trust policy it is allowed to rewrite. None of those policies looks dangerous on its own. The danger is in the combination, across principals and across accounts.

This article introduces escalint, an open-source Go tool I built to find those combinations: it reads an IAM snapshot of one or more AWS accounts, evaluates the policies the way AWS does, builds a graph of who can act as whom and shows, visually and with an explanation, every path that ends at administrator, along with the smallest set of changes it can find to cut them.

There is a live demo with synthetic data that runs entirely in your browser.

The current landscape

The offensive side is very well served

Attackers targeting AWS have modern, maintained tools. Pacu, by Rhino Security Labs, is an exploitation framework with modules that enumerate permissions and carry out the escalation. CloudFox, by Bishop Fox, maps an account's attack surface from compromised credentials. Stratus Red Team, by Datadog, emulates cloud attack techniques mapped to MITRE ATT&CK. That ecosystem keeps up with every new service and every newly published technique.

The defensive side, not so much

For defenders, effective-access analysis in IAM is a much weaker space:

  1. Prowler: the reference open-source CSPM, and rightly so. But it is broad by design: hundreds of configuration checks across several providers (public S3, open security groups, encryption, logging). Its escalation checks look at one policy at a time for dangerous action combinations. It does not answer "can this principal, by assuming that role, reach admin in two steps?".
  2. PMapper: NCC Group's project showed that IAM is best understood as a graph and pioneered the approach. It has not seen meaningful updates in years, and its catalog of techniques and services has fallen behind.
  3. CloudMapper and Cloudsplaining: Duo Labs' CloudMapper stopped in 2022. Salesforce's Cloudsplaining produces good reports on overly broad policies, but analyzes each policy in isolation and has an uncertain future.

In December 2025, Datadog published pathfinding.cloud, an open catalog of AWS privilege escalation paths. At launch, of the 65 documented paths, 27 (42%) were not detected by any open-source tool evaluated. The catalog now lists 92 paths. In other words: the attacker has a framework that runs the technique, and the defender often has nothing that flags it beforehand.


Why see paths visually

John Lambert, of Microsoft, put it well: "Defenders think in lists. Attackers think in graphs. As long as this is true, attackers win." Active Directory went through exactly this. BloodHound started as an offensive tool and was adopted by defense teams, because a graph of attack paths shows what no list of findings can.

AWS IAM has the same shape. A list says "role X has iam:PassRole on *" (medium severity, lost among two hundred others). A graph says "the CI user can launch an EC2 instance with role Y, which can attach a policy to itself, which makes it account admin". The first is a finding; the second is a path, with a beginning, a middle and an end, and an obvious place to cut.

Seeing the path also changes the fix. When ten paths go through the same edge, that edge is a choke point: removing it solves ten problems at once. In a list, those ten show up as ten unrelated items.

escalint Trace view: a two-step path to admin, with the required actions, the pathfinding.cloud ID and the CloudTrail visibility of each step

How it works

 escalint collect (read-only)  or  aws iam get-account-authorization-details
                          │
                strict snapshot parsing ─────────── untrusted data, size limits
                          │
                IAM policy evaluator ────────────── explicit deny, SCP, RCP, boundary,
                          │                          trust policy, cross-account; trace per decision
                          ▼
               "can act as" graph ◄──────────────── 39 rules add escalation edges
                          │
     ┌──────────┬─────────┼──────────┬──────────────┐
     ▼          ▼         ▼          ▼              ▼
  k paths     tiers   blast radius  choke points  fix plan
  to Tier 0                                       (greedy)
                          │
     analysis.json ──► text · JSON · SARIF · DOT · Mermaid · embedded web UI

Faithful evaluation

The core is an evaluator that follows AWS's documented logic: explicit deny in any policy, SCPs and RCPs by OU level, permissions boundaries, identity policies, trust policies and cross-account rules, including NotAction, NotResource, NotPrincipal and wildcards in actions and ARNs. Every Allow comes with a trace naming the statement that decided it. Conditions that cannot be evaluated statically (MFA, source IP, time of day) are not dropped: the step becomes conditional and is drawn dashed in the graph.

The graph and the rules

Nodes are principals (users, roles, groups) plus one sink per account, Tier 0 (effective admin). Edges come from 39 rules: 30 escalation paths (EL-PE), 6 trust misconfigurations (EL-TR, e.g. GitHub Actions OIDC without a sub condition) and 3 input diagnostics (EL-PARSE). Every rule cites the AWS documentation, its pathfinding.cloud ID and the matching MITRE ATT&CK techniques. The current version covers 33 of the catalog's 92 paths.

Analyses

On top of the graph, escalint computes the k shortest paths from each principal to Tier 0, privilege tiers, blast radius, assume-role cycles, choke points and a greedy fix plan: an ordered sequence of changes that removes every path. For each step it also says which CloudTrail events would be recorded, or whether that step is a blind spot.


The interface

The UI is a strict-TypeScript React app with its own WebGL renderer, embedded in the binary. It runs locally with escalint serve (loopback only, with a one-time token) or as a single offline HTML file with escalint report --html. There are nine views:

  1. Overview: tiers, critical paths, CloudTrail blind spots and what to fix first.
  2. Atlas: every principal in the account (or organization), grouped by account, with semantic zoom; Shift+click two nodes to see the shortest path between them.
  3. Trace: one path step by step, with alternatives and choke points. Clicking an edge opens the policy explainer, with the exact statement that allowed that step.
  4. Reach: what a principal can reach, and who can reach it, in rings by hop.
  5. Exposure, Findings, Fix plan, Detect and Diff: a principal × service matrix, a filterable table, the fix plan with a live preview of the remaining paths, CloudTrail events per step and what changed between two snapshots.
escalint Atlas view: every principal in the account in a WebGL graph

Every graph has an equivalent table behind it, navigation works from the keyboard and the palettes are safe for color-vision deficiencies. Since the snapshot comes from an account that may contain hostile names and policies, the UI uses a strict CSP and never uses innerHTML, and --redact replaces every identifier with a pseudonym before a report is shared.

escalint Fix plan view: ordered changes and the paths that remain after each one

Hands-on

Release binaries (Linux, macOS and Windows, amd64 and arm64) ship with the UI embedded and are signed with cosign, with GitHub provenance attestations. There is also a distroless image:

# pick your platform at https://github.com/brennocm/escalint/releases
curl -sSLO https://github.com/brennocm/escalint/releases/download/v0.1.0/escalint_0.1.0_linux_amd64.tar.gz
gh attestation verify escalint_0.1.0_linux_amd64.tar.gz --repo brennocm/escalint
tar xzf escalint_0.1.0_linux_amd64.tar.gz escalint

docker run --rm -v "$PWD:/work" ghcr.io/brennocm/escalint:v0.1.0 scan /work/snapshot.json

The basic flow is three commands. collect only makes read calls (List* and Get*), and the repository publishes the minimal IAM policy it needs:

escalint collect -o snapshot.json   # read-only snapshot with your AWS credentials
escalint serve snapshot.json        # opens the UI on 127.0.0.1 with a one-time token
escalint scan snapshot.json         # findings in the terminal; exit 1 on high or critical

No credentials at hand? The repository ships example accounts. The real (abridged) scan output on the vulnerable one:

escalint scan examples/vulnerable-account/snapshot.json
1 account(s), 66 node(s), 114 edge(s), 15 path(s) to Tier 0; 19 critical, 28 high, 57 medium, 2 info

arn:aws:iam::111111111111:role/pe002-attacker (role, tier 1)
  path p_a9433adab2c37415 [CRITICAL, allowed, 1 step(s)]
    1. arn:aws:iam::111111111111:role/pe002-attacker --[modify_permissions EL-PE-002]--> effective admin (111111111111)
  CRITICAL EL-PE-002 Put an inline policy on its own role
    arn:aws:iam::111111111111:role/pe002-attacker can act as effective admin of account 111111111111 using iam:PutRolePolicy
    policy: inline:arn:aws:iam::111111111111:role/pe002-attacker/p statement #0 (snapshot line 766)
    ATT&CK: T1098.003 Account Manipulation: Additional Cloud Roles
    fix: Remove iam:PutRolePolicy from the role's policies, or scope its Resource to roles other than itself
         and add a permissions boundary that denies it.

arn:aws:iam::111111111111:role/tr002-ci-deploy (role, tier 3)
  CRITICAL EL-TR-002 GitHub Actions OIDC trust without a sub condition
    any GitHub Actions workflow, in any repository, can assume arn:aws:iam::111111111111:role/tr002-ci-deploy:
    the trust has no condition on token.actions.githubusercontent.com:sub
    policy: trust:arn:aws:iam::111111111111:role/tr002-ci-deploy statement #0 (snapshot line 1170)
    ATT&CK: T1199 Trusted Relationship; T1078.004 Valid Accounts: Cloud Accounts

Each finding points to the policy and the snapshot line that cause it, the ATT&CK techniques and a concrete fix. Exit codes make escalint usable as a gate: 0 no findings above --fail-on (default high), 1 findings and 2 usage error.

In CI

The use case I care about most is stopping a new path from appearing unnoticed. escalint diff compares two snapshots and fails only on added edges, paths and findings. The repository ships a GitHub Action that, in a daily job, collects with a read-only role through OIDC, compares with yesterday's snapshot and uploads the result as SARIF to GitHub Code Scanning:

escalint diff baseline.json snapshot.json --fail-on high
escalint scan snapshot.json --format sarif --output escalint.sarif

Where escalint fits

escalint does not replace any of the tools above. Prowler is still the place for overall account posture; escalint deliberately ignores public buckets and open security groups. Pacu and CloudFox are still the place for exploitation and offensive validation; escalint never assumes roles, never creates credentials and never changes anything in the account.

The space it fills is PMapper's: defensive, static, explainable effective-access analysis for IAM, now with SCPs and RCPs, an up-to-date catalog mapped to pathfinding.cloud, CI-ready output and an interface designed so the defender sees the graph the attacker already sees. The idea of a self-hosted graph where no data leaves the account came from aws-iam-graph, by marcelogsantos; escalint is a from-scratch reimplementation.


Limitations

v1 does not model session policies and session tags, resource-based policies other than role trust (S3 bucket policies, KMS key policies, Lambda, Secrets Manager), root users or IAM Identity Center assignments. Request conditions cannot be known statically, so the affected steps stay conditional. Coverage is 33 of pathfinding.cloud's 92 paths; most of the rest pass a role to another compute service and follow the same template as the existing rules. The resource inventory is regional, the fix plan is greedy (small, not guaranteed minimal) and a snapshot is a point in time: after changes, collect again.

One important caveat: snapshots and reports are, quite literally, a map of who can become administrator. Treat them as sensitive data and share them only with --redact.


Final thoughts

IAM privilege escalation is rarely one obviously wrong policy. It is a sum of reasonable permissions, spread across different principals and accounts, that nobody reviewed together. Attackers have tools to find and walk that sum; defenders, most of the time, have a list.

escalint tries to even that out: evaluate policies the way AWS does, draw the graph, explain every edge with the statement that allows it and say where to cut. It is no guarantee that an account is safe, but it is a way to look at IAM the same way whoever attacks it will.