Cloudsplaining and the AWS IAM least privilege backlog
Cloudsplaining is an AWS IAM Security Assessment tool that identifies violations of least privilege and generates a risk-prioritized report.
At a glance
- What is it?
- Cloudsplaining audits AWS IAM policies across one account, many accounts or a single file, and sorts what it finds into five risk classes. The scanner is Python, the HTML report is a Vue build, and the packaging files disagree with each other about versions and terms.
- Who is it for?
- Cloudsplaining belongs in any account review where nobody will read every IAM policy by hand, because its five risk classes turn a large sprawl into a ranked list you can work through. It reports and does not repair, and Policy Sentry is still the project for writing least privilege policies from scratch.
- Can I use it commercially?
- Yes. BSD-3-Clause is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly JavaScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
Every policy statement lands in one of five risk classes
Cloudsplaining is an AWS IAM security assessment tool that identifies violations of least privilege and generates a risk-prioritized HTML report. Its unit of work is the policy document: each statement is examined for actions that carry no resource constraint, and every finding is filed under one of five risk classes. Data Exfiltration names actions such as `s3:GetObject`, `ssm:GetParameter` and `secretsmanager:GetSecretValue`. Infrastructure Modification covers permissions that change infrastructure. Resource Exposure is defined more narrowly, as the ability to modify resource-based policies. Privilege Escalation is derived from Pathfinding.cloud, and Credentials Exposure closes the list with no further description of what it gathers.
Scope reaches past a single account. The scanner can read all the policies in one AWS account, span multiple accounts, or take one policy file, which means the same rules serve a pull request review and a quarterly sweep across an estate. Findings arrive ranked by risk rather than listed flat, so the policies granting unrestricted privilege sit at the top of the report.
A published example at opensource.salesforce.com/cloudsplaining/ shows the finished HTML, and the README points there for the shape of a completed run.
Roles assumable by compute services are flagged above user roles
Policy documents are not the only thing checked. Cloudsplaining also identifies IAM roles that can be assumed by AWS Compute services such as EC2, ECS, EKS or Lambda, and rates them above user-defined roles. The stated reason is exposure: when the compute service sits on an instance that is directly or indirectly reachable from the internet, whatever that role can do becomes reachable with it.
The worked case is Systems Manager. An attacker holding `ssm:SendCommand` against privileged EC2 instances running the SSM agent inherits, in effect, the privileges of those instances. Remote Code Execution via AWS Systems Manager Agent is already a known escalation and exploitation path, and these role findings exist to surface the accounts where it applies. The README is explicit that the output is useful to penetration testers as well as attackers, in certain scenarios.
What the tool does not supply is proof of reachability. It lists assumable roles found in the account and leaves the judgement about whether a given one is genuinely exposed to you, which is exactly the case the exclusions file exists to handle, so an intentionally public compute role stops appearing in the report.
Policy Sentry writes the fix, Cloudsplaining finds the backlog
Cloudsplaining was written to close one gap. Policy Sentry, a companion project from the same engineering group, makes writing least privilege policies straightforward, and the README argues its adoption starts quickly once Infrastructure as Code developers and AWS administrators are comfortable with it. What remains afterwards is the backlog: accounts that have run for years hold a large number of documents nobody is rewriting, and the question becomes how to find them programmatically.
The README draws the defect with a wildcard statement. A principal holding `s3:PutObject` on `*` can write to every bucket in the account:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "*"
}
]
}The replacement constrains the resource to a bucket ARN:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::my-bucket/*"
}
]
}One detail hides in that pair. The second document grants `s3:GetObject` rather than the `s3:PutObject` of the first, so the two snippets differ in the action as well as in the resource. They are not a before and after of a single statement, and what they demonstrate is resource scoping rather than a permission change. Cloudsplaining works on the left side of that problem, locating the wildcards already sitting in an account and ranking them by what each one exposes.
Installing through brew tap or pip3, and what each route pulls in
Two install routes are given, and both put the same command on your PATH.
brew tap salesforce/cloudsplaining https://github.com/salesforce/cloudsplaining
brew install cloudsplainingpip3 install --user cloudsplainingThe formula is maintained in the HomebrewFormula/ directory at the top of the repository, so the tap points at a real checkout rather than at a packaged binary. After either route, `cloudsplaining --help` prints the available subcommands, which is the quickest way to see the current command surface without reading the documentation site.
Completion takes one line in your shell startup file:
eval "$(_CLOUDSPLAINING_COMPLETE=bash_source cloudsplaining)"For zsh the same expression with zsh_source in place of bash_source goes into .zshrc. On the Python side, pyproject.toml sets `requires-python = ">=3.10"` and the classifiers run from Python 3.10 through 3.14, so a host on an older interpreter sits outside the range the pip3 route supports. Dependency bounds are tight at both ends: boto3 and botocore hold `>=1.41.0,<2.0.0`, and Policy Sentry's library is required at `policy_sentry>=0.15.0,<0.16.0`, so a version outside 0.15.x is not pulled in during an upgrade.
Scanning one policy file needs no account and no credentials
The shortest real run works on a single document, which makes this the mode to use in review before anyone hands over account access.
cloudsplaining scan-policy-file --input-file examples/policies/explicit-actions.jsonOutput arrives as an issue class followed by the actions behind it:
Issue found: Data Exfiltration
Actions: s3:GetObject
Issue found: Resource Exposure
Actions: ecr:DeleteRepositThat second action name ends mid-word in the sample, printed as `ecr:DeleteReposit` rather than the complete repository action, so treat the console block as an illustration of the format rather than a string to match against. Fixtures ship for exactly this mode. Under `examples/` sit `policies/`, `files/`, `scripts/`, `jira-tickets/` and `example-exclusions.yml`, so a new user can produce a report and compare it against the shipped files without touching a real account.
One gap shows up in the command surface. Apart from this single policy subcommand, the account wide entry points are documented on the ReadTheDocs site rather than in the repository README, so read the hosted documentation before scheduling a full sweep.
An exclusions file filters the findings you already know about
Ranking risk produces false positives, and Cloudsplaining answers them with a custom exclusions file that filters results before the report is read. The reasoning given is an asymmetry in how IAM objects are written: user policies are permissive by design, whereas system roles are generally more restrictive, so one threshold is too loud for one group and too quiet for the other. Exclusions can also encode the specifics of a multi-account strategy or an application architecture, which makes the file an editorial statement about an estate as much as a configuration.
A starting point ships in the repository as `examples/example-exclusions.yml`, and the dependencies behind that path are `pyyaml>=6.0.0,<7.0.0` for YAML plus `schema>=0.7.0,<0.8.0`. Two consequences follow from keeping such a list. Entries tuned to one account layout do not transfer cleanly to another, so the file needs review whenever the topology changes. And the README documents no rollback behaviour for an exclusions file that has suppressed too much, which is a reason to diff that file rather than trust it.
Findings are grouped by the five risk classes rather than emitted as one flat queue, so an exclusion maps to a class and a set of actions instead of a single line of output.
A Vue report front end and a stale Dockerfile share one package
Cloudsplaining ships two toolchains, and where they meet is unusual. The command line is Python: pyproject.toml registers `cloudsplaining = "cloudsplaining.bin.cli:main"` as the console entry point and carries version 0.9.1. The HTML report is a Vue application built with Vue CLI, using chart.js and vue-chartjs for the charts and bootstrap for styling.
package.json describes only that front end. It is marked `"private": true`, its version field still reads 0.4.8 against the Python package's 0.9.1, and `main` points at `cloudsplaining/output`. The production build writes into `cloudsplaining/output/dist` with `--no-clean`, and the mocha target runs `mocha --recursive cloudsplaining/output/src/test/`, which places front-end tests inside the Python package directory. Declared runtimes disagree as well: package.json requires `"node": ">=20"` while the Dockerfile installs nodejs 18.16.0.
The Dockerfile is worth reading before building anything from this tree. It pins `FROM --platform=linux/amd64 alpine:3.17.2`, installs python3==3.10.11-r0, then runs `python3 -m pip install -r requirements.txt` before `npm install` and an ENTRYPOINT of cloudsplaining. No requirements.txt appears in the top-level tree, where packaging sits in pyproject.toml and the build runs through `uv build`, so the image recipe still follows an older layout.
One inconsistency needs settling before anyone vendors the scanner: the LICENSE file at the top level is a BSD-3-Clause text while pyproject.toml declares `license = "MIT"`.
Five just tasks stand between a change and the default branch
The repository carries its own gate in a justfile, with recipes grouped as docs, js, package and test. The one that matters before a change lands is `pre-push`, commented in the file as the full local gate mirroring CI, and it chains five tasks:
uvx pre-commit run --all-files
uv run coverage run -m pytest -v
uv run ty check
uv run ./utils/safety_scan.pyUnderneath sit two recipes specific to this project rather than generic hygiene. `safety-scan` executes `uv run ./utils/safety_scan.py`, and `check-sampledata-sync` runs `uv run pytest test/test_sample_data_in_sync.py -q` to fail when sampleData.js and utils/example-iam-data.json drift apart. That last check also explains how the published example report exists at all: it is rendered from fixture data committed to the repository rather than captured from a live AWS account. Two reports can be diffed directly through `uv run ./utils/compare_example_reports.py --old {{ old }} --new {{ new }}`.
For anyone tracking upkeep instead of contributing, two dates are worth keeping: the last push was on 2026-09-18, and the newest listed tag is 0.9.1 from 2026-06-14. Both land within a few months of each other. For a consumer, an upgrade is bounded by the pinned ranges in pyproject.toml, and no upgrade or migration procedure for an existing exclusions file appears anywhere in the README.
Editorial conclusion
Cloudsplaining belongs in any account review where nobody will read every IAM policy by hand, because its five risk classes turn a large sprawl into a ranked list you can work through. It reports and does not repair, and Policy Sentry is still the project for writing least privilege policies from scratch. Check three things before you adopt it: that your interpreter is Python 3.10 or newer, what you intend to put in the exclusions file, and whether the account wide subcommands on the documentation site match the multi account layout you actually run.
Frequently asked questions
What does Cloudsplaining check in an AWS account?
It identifies least privilege violations in IAM policies and files each finding under one of five risk classes: Data Exfiltration, Infrastructure Modification, Resource Exposure, Privilege Escalation and Credentials Exposure. It can scan all policies in one account, span multiple accounts, or read a single policy file.
How do I install Cloudsplaining?
Two routes are given: tapping the Homebrew formula and running `brew install cloudsplaining`, or installing the Python package with `pip3 install --user cloudsplaining`. Either way, `cloudsplaining --help` prints the available subcommands.
What Python and Node versions does Cloudsplaining need?
pyproject.toml sets `requires-python = ">=3.10"` and lists classifiers from Python 3.10 through 3.14. Node matters only for building the Vue report: package.json requires `"node": ">=20"`, while the Dockerfile installs nodejs 18.16.0.
How do I reduce false positives in a Cloudsplaining report?
Supply a custom exclusions file that filters out findings which are correct but irrelevant. A starting point ships as `examples/example-exclusions.yml`, and exclusions can encode a specific multi-account strategy or application architecture.
Does Cloudsplaining fix the IAM policies it flags?
No. It generates a risk-prioritized HTML report, and the README presents Policy Sentry as the project for writing least privilege policies. Cloudsplaining locates the backlog that needs that work.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/salesforce-cloudsplaining)Was this useful?