Skip to main content

iam-to-english logo

iam-to-english

Translate AWS IAM policies into plain English, and catch privilege escalation before it ships.

PyPI version Python versions Docker pulls MIT license

A JSON IAM policy is read and turned into a plain-English summary with its risk level

iam-to-english reads an IAM policy (identity policy, role trust policy, S3 bucket policy...) and tells you, in plain English, what it allows, who it allows, and what could go wrong: privilege escalation paths across statements, public access, wildcard abuse, risky trust relationships. It runs 100% offline and deterministically, works in CI as a quality gate, and can optionally ask an LLM for a deeper review (local Ollama, Amazon Bedrock, the Anthropic API or the OpenAI API).

$ iam-to-english examples/privilege-escalation.json
Policy: examples/privilege-escalation.json
2 statements | Services: IAM, Lambda
Risk: CRITICAL (1 privilege escalation path)

IN SHORT:
  #1 DeployLambdas: Can create Lambda functions, invoke Lambda functions and get Lambda function
      details, on all resources.
  #2 PassAnyRole: Can pass an IAM role to an AWS service, on all resources.

STATEMENT BY STATEMENT:

ALLOW #1 (DeployLambdas)
   Actions:
     - Create Lambda functions
       lambda:CreateFunction
     - Invoke Lambda functions
       lambda:InvokeFunction
     - Get Lambda function details
       lambda:GetFunction
   Resources:
     - All resources (no restriction)

ALLOW #2 (PassAnyRole)
   Actions:
     - Pass an IAM role to an AWS service
       iam:PassRole
   Resources:
     - All resources (no restriction)

PRIVILEGE ESCALATION PATHS (ways to gain more access than granted):

  [CRITICAL] PassRole + Lambda Code Execution
    Found in: Statement #1 (DeployLambdas), Statement #2 (PassAnyRole)
    Risk: Can pass a high-privilege IAM role to a new or modified Lambda function and invoke it to
      execute arbitrary commands.
    Permissions involved: iam:PassRole, lambda:CreateFunction, lambda:InvokeFunction
    How to fix: Restrict iam:PassRole using 'iam:PassedToService' condition to specific services and
      scope Resource to non-privileged roles.

  Run with --verbose to see the attack steps.

Why use it

  • Readable by humans. Reviewers who do not speak IAM can understand a policy in seconds. Every report opens with one plain sentence per statement ("Anyone (public access) can download objects from S3, on all objects in S3 bucket 'my-website'."), then details each statement: arn:aws:s3:::prod-data-*/* becomes "all objects in S3 buckets matching 'prod-data-*'" and {"Bool": {"aws:SecureTransport": "false"}} becomes "Request does NOT use HTTPS". Findings are sentences too, naming the statement they come from.
  • Finds what linters miss. 21 known privilege escalation paths (Rhino Security Labs research and follow-ups) are detected across statements, while honouring NotAction, explicit Deny, iam:PassedToService, resource scoping and ${aws:username} self-service patterns.
  • Low noise. Tested against all 1,668 AWS managed policies: ViewOnlyAccess and SecurityAudit come out CLEAN, ReadOnlyAccess only gets MEDIUM notes about secret-reading actions (ec2:GetPasswordData, ssm:GetParameter), while AdministratorAccess, IAMFullAccess and AWSLambda_FullAccess are CRITICAL. A PassRole limited to one named role is reported as MEDIUM with a note to review that role, not as CRITICAL.
  • Built for CI. --fail-on HIGH, stable exit codes, JSON output, Markdown for PR comments, a semantic --diff between two policy versions, a GitHub Action and a pre-commit hook.
  • Private by default. No network calls unless you fetch a policy from AWS or opt in to an AI review.

Installation

Requires Python 3.10+.

# pipx (recommended: isolated, puts iam-to-english on your PATH)
pipx install iam-to-english

# pip
pip install iam-to-english

# Upgrade
pipx upgrade iam-to-english            # or: pip install --upgrade iam-to-english

# Latest development version
pipx install git+https://github.com/kloudsec-io/iam-to-english

# From a clone (for contributors, see CONTRIBUTING.md)
git clone https://github.com/kloudsec-io/iam-to-english && cd iam-to-english
pip install -e ".[dev]"

No Python? Use the Docker image.

Check the install with iam-to-english --version.

Usage

Analyze a policy

iam-to-english policy.json              # JSON or YAML file
iam-to-english policy.yaml
iam-to-english policies/*.json          # several files at once (summary + combined exit code)
cat policy.json | iam-to-english -      # stdin
iam-to-english '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}'

Raw AWS CLI output works as-is, no --query needed. The tool unwraps get-policy-version, get-role, get-role-policy and get-bucket-policy responses (and URL-encoded documents):

aws iam get-policy-version --policy-arn arn:aws:iam::123456789012:policy/MyPolicy --version-id v3 | iam-to-english -
aws iam get-role --role-name deploy | iam-to-english -          # the role's trust policy
aws s3api get-bucket-policy --bucket my-bucket | iam-to-english -

Fetch directly from AWS

Uses your normal AWS credentials (-p/--profile to pick a profile):

iam-to-english arn:aws:iam::aws:policy/ReadOnlyAccess -p myprofile   # managed policy (default version)
iam-to-english arn:aws:iam::123456789012:role/deploy                  # a role ARN = its trust policy
iam-to-english --role-name MyRole --trust-policy                      # same, by name
iam-to-english --role-name MyRole InlinePolicyName -p prod            # an inline role policy
iam-to-english s3://my-bucket                                         # a bucket policy

Output formats

iam-to-english policy.json                 # colored text (plain when piped or NO_COLOR is set)
iam-to-english policy.json -v              # + step-by-step attack paths for escalation findings
iam-to-english policy.json -f json         # machine-readable, includes summary.risk_level
iam-to-english policy.json -f markdown     # for PR comments / GitHub job summaries

What changed between two versions? (--diff)

iam-to-english old-policy.json --diff new-policy.json

The diff is semantic, not textual: it compares (action, resource) grants, so reordering statements or changing the case of an action name is not a change, while an action quietly moving from one bucket to "Resource": "*" is flagged:

$ iam-to-english old.json --diff new.json
IAM Policy Diff: old.json -> new.json
=====================================

RISK INCREASED: MEDIUM -> CRITICAL

NEW PRIVILEGE ESCALATION PATHS:
  + [CRITICAL] PassRole + EC2 Instance Profile
    Risk: Can launch an EC2 instance with an existing privileged IAM instance profile and access credentials via IMDS.
    Permissions involved: iam:PassRole, ec2:RunInstances

Permissions Added (+):
  + Launch EC2 instances (ec2:RunInstances), on all resources
  + Pass an IAM role to an AWS service (iam:PassRole), on all resources

Permissions Removed (-):
  - Launch EC2 instances (ec2:RunInstances), on arn:aws:ec2:eu-west-1:123456789012:instance/*

Widened to All Resources (!):
  ! Launch EC2 instances (ec2:RunInstances) now applies to all resources

Deny Guardrails Removed (!):
  - Every IAM action (iam:*), on all resources

It also reports removed/added Deny statements, relaxed or added conditions, new and resolved warnings, and new and removed escalation paths. With --fail-on, the gate applies to the new policy's risk level.

Docker

The image is kloudsecio/iam-to-english on Docker Hub, built for linux/amd64 and linux/arm64. The entrypoint is iam-to-english, so everything after the image name is a normal CLI argument.

# Analyze a local file: mount the folder read-only at /data
docker run --rm -v "$(pwd):/data:ro" kloudsecio/iam-to-english /data/policy.json

# Options work as usual
docker run --rm -v "$(pwd):/data:ro" kloudsecio/iam-to-english /data/policy.json -f markdown --fail-on high

# Compare two versions
docker run --rm -v "$(pwd):/data:ro" kloudsecio/iam-to-english /data/old.json --diff /data/new.json

# From stdin (note -i)
cat policy.json | docker run --rm -i kloudsecio/iam-to-english -

# Inline JSON
docker run --rm kloudsecio/iam-to-english '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":"s3:GetObject","Resource":"*"}]}'

The container exit code is the CLI exit code (see Exit codes), so docker run ... --fail-on high works as a CI gate.

Fetching from AWS

The container runs as root, so mount your AWS config at /root/.aws:

docker run --rm -v ~/.aws:/root/.aws:ro kloudsecio/iam-to-english \
  arn:aws:iam::aws:policy/ReadOnlyAccess -p myprofile

docker run --rm -v ~/.aws:/root/.aws:ro kloudsecio/iam-to-english \
  --role-name MyRole InlinePolicyName -p myprofile

docker run --rm -v ~/.aws:/root/.aws:ro kloudsecio/iam-to-english \
  s3://my-bucket -p myprofile

Or pass credentials as environment variables: -e AWS_ACCESS_KEY_ID -e AWS_SECRET_ACCESS_KEY -e AWS_SESSION_TOKEN -e AWS_REGION.

AI review

# Amazon Bedrock
docker run --rm -v "$(pwd):/data:ro" -v ~/.aws:/root/.aws:ro kloudsecio/iam-to-english \
  /data/policy.json --ai -p myprofile --ai-region eu-west-1

# Anthropic API (the key is passed by name, so it never appears in the command)
docker run --rm -v "$(pwd):/data:ro" -e ANTHROPIC_API_KEY kloudsecio/iam-to-english \
  /data/policy.json --anthropic

# Ollama running on your host: localhost inside the container is the container itself
docker run --rm -v "$(pwd):/data:ro" kloudsecio/iam-to-english \
  /data/policy.json --ollama --ollama-url http://host.docker.internal:11434

Build it yourself

docker build -t iam-to-english .
docker run --rm iam-to-english --version

Use it in CI

Exit codes

Code Meaning
0 Success, and no finding at or above --fail-on
1 A finding (warning or escalation path) is at or above --fail-on
2 Invalid input (missing file, not a policy, malformed statement), AWS/AI error, or bad usage

--no-warnings only hides warnings from the output; --fail-on still evaluates them.

GitHub Action

# .github/workflows/iam.yml
name: IAM policy review
on: pull_request
jobs:
  iam:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: kloudsec-io/iam-to-english@main   # or pin a release tag / commit SHA
        with:
          files: "policies/*.json"
          fail-on: HIGH          # or CRITICAL / MEDIUM / LOW, or "" to never fail
          format: markdown       # rendered in the job summary

pre-commit

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/kloudsec-io/iam-to-english
    rev: main          # pre-commit recommends pinning a tag or commit SHA
    hooks:
      - id: iam-to-english
        # args: [--fail-on, CRITICAL]     # default is HIGH
        # files: ^infra/iam/.*\.json$     # default: files under iam/ or policies/, or named *policy*

Any other CI

pip install iam-to-english
iam-to-english policies/*.json --fail-on HIGH -f markdown > iam-report.md

What it checks

Privilege escalation paths (cross-statement)

Path Severity
iam:PassRole + Lambda create/update + invoke (or event source / function URL) CRITICAL
iam:PassRole + ec2:RunInstances CRITICAL
iam:PassRole + CloudFormation CreateStack/UpdateStack CRITICAL
iam:PassRole + Glue dev endpoint / job CRITICAL
iam:PassRole + CodeBuild project + start build CRITICAL
iam:PassRole + ECS register task definition + run/start/service CRITICAL
iam:Attach{User,Role,Group}Policy CRITICAL
iam:Put{User,Role,Group}Policy CRITICAL
iam:UpdateAssumeRolePolicy (trust policy backdoor) CRITICAL
iam:PassRole + Data Pipeline / SageMaker notebook HIGH
iam:CreateAccessKey, iam:Create/UpdateLoginProfile (on other users) HIGH
iam:AddUserToGroup, iam:CreatePolicyVersion / SetDefaultPolicyVersion HIGH
Permissions boundary removal/replacement HIGH
lambda:UpdateFunctionCode, glue:UpdateDevEndpoint, codebuild:StartBuild (hijack existing resources) HIGH
ssm:SendCommand / ssm:StartSession, ec2-instance-connect:SendSSHPublicKey HIGH

How findings are qualified:

  • Actions granted through wildcards (iam:*, iam:Put*, "*") or NotAction count; the output says which pattern granted them (Granted by: NotAction s3:*).
  • An unconditional Deny on "*" removes the path. Conditional denies are not assumed to apply.
  • iam:PassedToService is honoured: PassRole limited to lambda.amazonaws.com does not enable the EC2 path.
  • "Action": "*" scoped to an S3 bucket is not an IAM escalation (resource relevance is checked).
  • PassRole limited to explicitly named roles is downgraded to MEDIUM with a note to review those roles; other paths limited to specific resources are downgraded one level.
  • iam:CreateAccessKey on ${aws:username} (manage your own keys) is not flagged; attaching policies to yourself still is.
  • A policy that is already full admin reports one CRITICAL finding, not 20 redundant paths.

Warnings

Check Severity
Action: "*" on Resource: "*" (full admin), or NotAction on "*" that still allows IAM CRITICAL
Public principal ("*" or {"AWS": "*"}) with write access, or able to assume a role CRITICAL
GitHub OIDC trust without a token.actions.githubusercontent.com:sub condition CRITICAL
Action: "*" on specific resources, NotAction/NotResource/NotPrincipal with Allow HIGH
Public read access, GitHub OIDC sub that allows any repo, OIDC trust without conditions HIGH
~50 known dangerous actions (e.g. iam:PassRole, s3:PutBucketPolicy, kms:ScheduleKeyDeletion), including when reached through a wildcard like ec2:Get* per action, one level lower when resource-scoped
Service-wide wildcards (iam:*, kms:*... HIGH; others MEDIUM) HIGH / MEDIUM
Write actions on "*" (least-privilege hint), no MFA condition on high-impact actions, policy version not 2012-10-17 LOW

Every report starts with a single Risk verdict: the highest severity found, or CLEAN.

AI-Powered Deep Analysis (optional)

The offline engine is fast, free and deterministic, and is always the first part of the report. An LLM adds what rules cannot know: business intent ("this looks like a CI/CD deploy role"), least-privilege rewrites, and context-dependent judgement. Pick one backend:

Mode Flag Credentials Default model
Offline (always on) none none none
Local LLM --ollama none (Ollama running locally) best installed model
Amazon Bedrock --ai your AWS credentials (-p / --ai-profile) Claude Haiku 4.5
Anthropic API --anthropic ANTHROPIC_API_KEY claude-opus-5-5
OpenAI API --openai OPENAI_API_KEY gpt-5

--ai-model overrides the model for any backend. API keys are read from the environment only, never from the command line, so they do not end up in your shell history. The policy is sent to the selected backend: prefer Ollama for sensitive policies.

With -f json the review is embedded in the report as an ai_analysis object (backend, model, text, or error), so the output stays a single valid JSON document; with -f markdown it is appended as an "AI Analysis" section. If the AI backend fails, the offline report is still printed and the exit code is 2.

Anthropic API

export ANTHROPIC_API_KEY=...
iam-to-english policy.json --anthropic
iam-to-english policy.json --anthropic --ai-model claude-haiku-5-5     # faster, cheaper

Uses the official anthropic SDK (ANTHROPIC_BASE_URL is honored for gateways). With the default model the request asks for a thorough review and enables server-side refusal fallbacks; the model that actually answered is shown in the "AI Analysis" header.

OpenAI API

export OPENAI_API_KEY=...
iam-to-english policy.json --openai
iam-to-english policy.json --openai --ai-model gpt-5-mini

OPENAI_BASE_URL is honored, so any OpenAI-compatible endpoint works too.

Amazon Bedrock (default)

iam-to-english policy.json --ai -p myprofile --ai-region eu-west-1
iam-to-english policy.json --ai --ai-model eu.anthropic.claude-sonnet-4-6 --ai-region eu-west-1

Default model is Claude Haiku 4.5 (fastest, cheapest). Requires Amazon Bedrock access with the model enabled, and a region: --ai-region, AWS_REGION / AWS_DEFAULT_REGION, or the region of your AWS profile.

Recent models can only be called on demand through a cross-region inference profile (us., eu., apac., global....). Give the plain model ID and the tool picks the right profile for your region by asking Bedrock (bedrock:ListInferenceProfiles); without that permission it tries the regional geography, then global., then the plain ID. An explicit profile ID or inference profile ARN passed to --ai-model is used as is. The model actually used is shown in the "AI Analysis" header.

Local Ollama (free, offline)

iam-to-english policy.json --ollama                       # picks an installed model automatically
iam-to-english policy.json --ollama --ai-model gemma3:4b

Without --ai-model, the tool asks Ollama which models are installed and prefers gemma3:4b, then llama3.1:8b, phi4:14b, mistral, then whatever is installed. The context window is sized to fit the whole policy (Ollama otherwise silently cuts long prompts) and the answer is capped at about 3,000 tokens, so a small model stuck in a loop cannot run forever. Install Ollama from ollama.com, then:

ollama serve
ollama pull gemma3:4b      # 3.3 GB -- best quality/size ratio for IAM analysis

How good are the AI reviews?

Measured on 2026-10-18 with the default settings, one run per policy, on five policies. The expected risk level was fixed before running: a 6-statement policy with conditions and a scoped PassRole (MEDIUM), full admin (CRITICAL), PassRole + Lambda (CRITICAL), PassRole + CodeBuild (CRITICAL), and a cross-account sts:AssumeRole trust policy protected by an ExternalId (LOW).

gemma3:4b (3.4 GB) llama3.1:8b (4.9 GB) Bedrock Claude Haiku 4.5
Risk level exactly right 1 / 5 2 / 5 3 / 5
Risk level within one step 4 / 5 5 / 5 5 / 5
PassRole + Lambda / CodeBuild escalation Described both Listed the pieces separately for Lambda, described CodeBuild Described both, with the attack path
ExternalId trust policy Called the ExternalId "highly insecure" (it is a recommended safeguard) Treated it as a safeguard Correctly called it good practice
Average time per policy 12 s 13 s 9 s
Cost Free, runs locally Free, runs locally Per-token pricing

This is a small sample, so read it as a rough guide. On the scoped 6-statement policy, gemma3:4b and Bedrock both rated it HIGH where MEDIUM was expected. The takeaway: a small local model is fine for a first look, but it can give wrong AWS-specific advice, so check its output. The offline engine does not depend on any model, and its escalation findings are what you should gate CI on.

For Bedrock costs, see the Amazon Bedrock pricing page.

What the offline engine cannot know

Gap Why
Whether a passed role is actually privileged The target role's policies are not part of the input; scoped PassRole is flagged MEDIUM for review
Effective permissions across policies SCPs, permission boundaries, session policies and other attached policies are not combined
Business intent "Is s3:* on this bucket expected for this workload?" needs context (try --ai)
Condition semantics in depth Conditions are translated and some are recognised as restricting, but not fully evaluated

Options

Usage: iam-to-english [OPTIONS] [SOURCE]...

  -f, --format [text|json|markdown]  Output format (default: text)
  -d, --diff TEXT                    Compare SOURCE against another policy (semantic diff)
  --fail-on [critical|high|medium|low]
                                     Exit 1 if findings meet or exceed this severity
  -v, --verbose                      Show step-by-step attack paths
  -p, --profile TEXT                 AWS profile (for fetching policies from AWS)
  --role-name TEXT                   IAM role name (for inline or trust policies)
  --trust-policy                     Fetch the role's trust (assume role) policy
  --s3-bucket TEXT                   Fetch the bucket policy of this S3 bucket
  --ai                               Add an AI review (default backend: Amazon Bedrock)
  --ollama                           AI review with a local Ollama model (implies --ai)
  --anthropic                        AI review with the Anthropic API, key in ANTHROPIC_API_KEY
  --openai                           AI review with the OpenAI API, key in OPENAI_API_KEY
  --ollama-url TEXT                  Ollama API URL (default: http://localhost:11434)
  --ai-region TEXT                   AWS region for Bedrock (default: from your AWS profile)
  --ai-model TEXT                    Model for the chosen AI backend
  --ai-profile TEXT                  Separate AWS profile for Bedrock
  --no-color                         Disable colored output
  --no-warnings                      Hide warnings from the output (--fail-on still uses them)
  --version                          Show version
  -h, --help                         Show help

Examples

The examples/ directory contains policies to try:

iam-to-english examples/s3-readonly.json               # clean, least-privilege
iam-to-english examples/dynamodb-policy.yaml           # YAML input
iam-to-english examples/complex-policy.json            # conditions, Deny, scoped PassRole (MEDIUM)
iam-to-english examples/privilege-escalation.json      # PassRole + Lambda (CRITICAL)
iam-to-english examples/overly-permissive.json         # full admin (CRITICAL)
iam-to-english examples/github-oidc-trust-policy.json  # any GitHub repo can assume the role
iam-to-english examples/public-bucket-policy.json      # public website bucket with TLS enforcement

Contributing

See CONTRIBUTING.md. Bug reports with a (sanitized) policy that produces a wrong or noisy result are the most valuable contribution.

To report a security vulnerability, see SECURITY.md.

License

MIT

Maintained by KloudSec.

Metadata

Release files for iam-to-english 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for iam-to-english 1.0.0
File Size Uploaded
iam_to_english-1.0.0.tar.gz 837.9 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for iam-to-english 1.0.0
File Interpreter ABI Platform
iam_to_english-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 898.4 kB

Release files / iam_to_english-1.0.0.tar.gz

Download URL iam_to_english-1.0.0.tar.gz
Size 837.9 kB
Tags Source
SHA-256 checksum
How to use checksums
f54e667ee10691fdeff5b158d5ba08c04ed2b9b17598d75507b964c855438be9
BLAKE2b-256 checksum
How to use checksums
27f8fd1ca157709b869d00a46825b62a23c37b49d2bafc5b66f51c34e70cc518
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release files / iam_to_english-1.0.0-py3-none-any.whl

Download URL iam_to_english-1.0.0-py3-none-any.whl
Size 60.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b8f28fce9d5bb0bbf9a19a811cd2e92ca2c2cb2f112e7b09e50a010c6bfd25b7
BLAKE2b-256 checksum
How to use checksums
bed6283d902ed792dd1d6daecbb56f4f383474b8103890d8b66f800b790f4352
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.12

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release 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