Show a semantic diff of two AWS IAM policies — what access actually changed, not what text changed.
A git diff on a policy tells you a line moved or a wildcard appeared. It does
not tell you that s3:Get* quietly became s3:*, that the new statement adds
iam:PassRole on *, or that a resource scope widened from one bucket to every
resource in the account. iam-policy-diff expands wildcards against an IAM
action catalog, computes the effective permission set of each policy, and
reports the delta — plus the privilege-escalation risks hiding in the additions.
- Parse both policy documents, tolerating IAM's scalar-or-array quirk
(
"Action": "s3:GetObject"vs a list) and a single-object-or-arrayStatement. - Expand every
Action/NotActionpattern (s3:Get*,iam:*,*) against an embedded action catalog into concreteservice:Actionnames. - Reduce to an effective set: each
Allowcontributes(action, resource, condition)grants; an explicit unconditionalDenysubtracts matching grants (explicit deny always wins). A condition change registers as remove + add, because the scope really did change. - Diff the two sets and flag additions that match known
privilege-escalation actions (
iam:PassRole,iam:AttachRolePolicy,sts:AssumeRole,lambda:UpdateFunctionCode, ...).
The catalog sits behind an interface, so the core logic is unit-tested with zero network access.
go install github.com/moveeeax/iam-policy-diff@latest
Or build from source:
git clone https://github.com/moveeeax/iam-policy-diff
cd iam-policy-diff
make build
iam-policy-diff [flags] OLD.json NEW.json
flags:
--json emit machine-readable JSON (for CI gates)
--fail-on-risk exit 2 if the new policy adds a risky permission
--no-color disable ANSI colour in human output
exit codes:
0 success 1 usage/IO error 2 risk gate tripped (--fail-on-risk)
Human output:
$ iam-policy-diff examples/old.json examples/new.json
Effective permission diff
+22 added -0 removed 2 risk(s)
Added
+ iam:PassRole on *
+ lambda:UpdateFunctionCode on *
+ s3:DeleteObject on arn:aws:s3:::my-app-data/*
...
⚠ Privilege risks
[high] iam:PassRole on * — Pass a role to a service — classic privilege escalation
[high] lambda:UpdateFunctionCode on * — Run arbitrary code in an existing function's role
Fail a pull request when a policy change introduces a risky permission:
- name: Guard IAM policy changes
run: iam-policy-diff --json --fail-on-risk policy/old.json policy/new.json--json prints a stable document with added, removed, risks, and a
summary, so you can also pipe it into jq for custom gates.
make fmtcheck # gofmt -l must be empty
make vet
make race # go test -race ./...
make build