Tarek Cheikh
Founder & AWS Cloud Architect
One profile. Every account. Read-only. And it tells you what it could not read.
In v1.5.0, awsmap got a brain: SQLite, SQL queries, natural-language questions.
Then it learned to remember. v1.7.0 added drift detection. v1.8.0 added waste detection, tag compliance and queries against any past scan.
There was still one thing missing. Many teams run more than one AWS account. Twenty. Or a hundred.
This is v1.9.0. awsmap now scans your whole AWS Organization.
Five releases, one idea: your AWS inventory should be a living dataset, not a one-shot report.
is_current column replaces heavy subqueries in the queries.awsmap diff. See what was added, removed or modified between two scans.awsmap waste, awsmap tags, and --scan to query any point in your history.awsmap org. The same inventory, for every account of your organization.Everything you already use works on the organization data without change: query, ask, examples, diff, tags, waste. One database. All accounts. These commands read the same local database whatever the number of accounts.
Give it the profile of your management account (or a delegated administrator). It lists the accounts, assumes a role in each member account, and scans them in parallel. The management account itself is read with your own profile credentials.
awsmap org -p org-root
Before the big run, check that awsmap can get into every account:
awsmap org -p org-root --dry-run
It tries to assume the role in every member account and tells you which ones fail. No resource is scanned, and the management account is not probed. The command exits with code 1 if any account is inaccessible.
Then the real thing, and the variations you will actually use:
# the whole organization
awsmap org -p org-root
# one organizational unit (child OUs included)
awsmap org -p org-root --ou ou-prod-12345678
# specific accounts
awsmap org -p org-root --accounts 111122223333,444455556666
# only some services and regions, in every account
awsmap org -p org-root -s ec2 -s s3 -r us-east-1 -r eu-west-1
# the scan was interrupted: skip the accounts finished in the last 24 hours
awsmap org -p org-root --resume
The default role name is OrganizationAccountAccessRole. Change it with --role-name, or once and for all with awsmap config set org_role_name <name>.
That role name is the one AWS Organizations creates automatically in accounts you create through Organizations. Accounts that were invited to the organization, or provisioned by other methods such as AWS Control Tower or Account Factory for Terraform, may have a role with a different name or no such role. Check how your accounts were provisioned, and run --dry-run first: it lists the accounts where the role cannot be assumed.
This is what the end of a run looks like (an excerpt, for 3 accounts and 5 services, to keep it short):
Organization Scan Complete!
Total Duration : 0.7 minutes (42.7s)
Accounts Scanned : 3
Accounts Partial : 0
Accounts Skipped : 0
Accounts Failed : 0
Resources Found : 567
Access denied : 6 call type(s) refused; resources behind them are not in the inventory
frauddetector GetDetectors: AccessDeniedException 3
frauddetector GetEventTypes: AccessDeniedException 3
frauddetector GetModels: AccessDeniedException 3
macie2 GetMacieSession: AccessDeniedException 3
rekognition DescribeProjects: AccessDeniedException 3
rekognition ListStreamProcessors: AccessDeniedException 3
Then query it like any other scan:
awsmap query -n resources-per-account-service
awsmap ask how many resources per account
awsmap diff
In the table output of query and ask, accounts show their name instead of a 12-digit ID.
I tested on a real organization of 103 accounts. About 25,000 resources. About 22 minutes.
The first surprise: the bottleneck was not CPU, and it was not AWS throttling. It was new TLS connections.
Opening a fresh connection for every API call topped out around 50 calls per second. Reusing a kept-alive connection, with the same threads, reached about 440 calls per second.
So awsmap org is built around one idea: reuse connections.
If your network is slow or shared, lower the total number of concurrent calls (the default is 256): awsmap org -p org-root -w 64.
Pointing a tool at every account of your organization deserves a hard question: what can it do there?
awsmap assumes the role in each member account with an inline session policy and the ReadOnlyAccess managed policy attached. AWS documents how session policies work: the permissions of the session are the intersection of the role's identity-based policies and the session policies, and an explicit deny in any of them overrides an allow. So when STS accepts the ReadOnlyAccess attachment, even a full administrator role gives the session no more than ReadOnlyAccess allows, minus an explicit deny on reading data. That deny covers ten actions: object contents (s3:GetObject*), secrets (secretsmanager:GetSecretValue), parameters (ssm:GetParameter*), kms:Decrypt, dynamodb:GetItem, dynamodb:BatchGetItem, dynamodb:Scan, dynamodb:Query, sqs:ReceiveMessage and kinesis:GetRecords.
awsmap is built to map your infrastructure, not to read what is inside it. Be precise about what the guardrail is, though: it is a read-only session with a short deny list on the most obvious data reads. ReadOnlyAccess still allows other read actions that are not on that list (for example, it currently allows ec2:Get*, logs:Get* and lambda:Get*), so a session policy does not turn the session into a metadata-only identity. If that matters in your environment, use a role that is read-only on its own, scoped to what you are willing to expose.
Three more things to know:
-s, the allow list is narrower: Describe*, List* and Get* on the chosen services only. If that list would exceed the 2,048-character limit for session policies, awsmap uses the wildcard form instead, which relies on ReadOnlyAccess for the read-only limit.The safe setup is a role that is read-only on its own, in every member account, so that nothing depends on the session policy being applied. Run --dry-run first, and treat the session policy as a second layer, not the only one.
If you really need the full role permissions, --no-session-policy turns the restriction off.
This is the part I care about most.
A scan that finishes with "success" and quietly misses things is worse than a scan that fails. You trust the inventory. You make decisions on it.
So v1.9.0 is explicit about every gap:
Retries. Throttled calls, 5xx errors and timeouts are detected on every API response. The whole task is re-run up to 3 times, with growing delays (15, 45 and 90 seconds), before awsmap gives up.
Partial accounts. If a task still fails after its retries, the account is stored as partial:
[2/2] PART 111122223333 (prod-account) -> 17 resources in 229.7s; 1 service(s) incomplete, not stored: lambda
The failed service is left out of that scan on purpose. Its previous rows stay current, so awsmap diff does not tell you that all your Lambda functions disappeared.
You can list them any time:
awsmap query "SELECT account_id, incomplete_services FROM scans WHERE incomplete_services IS NOT NULL"
Run with --resume and awsmap scans the partial accounts again (in full), and skips the accounts whose latest scan in the last 24 hours had no incomplete service. It does not check which services or regions that scan covered, so after a narrower scan an account can be skipped. The command also exits with code 2 when any account is partial, so your CI notices. Accounts that failed or were skipped, and access-denied gaps, do not change the exit code, so read the summary as well.
Access denied. The summary counts every permission error per service and call, and how many accounts hit it. An access-denied call does not make an account partial, which is why the summary lists them separately. You see which parts of your organization the inventory is blind to.
In real life, a throttled ListTags call in one region of one account was enough to trigger a partial scan. That is exactly the kind of thing that used to disappear without a trace.
While preparing this release I fixed four collectors that were returning incomplete or wrong data:
aiobotocore dependency is gone.us-east-1. AWS lists that restriction under the InvalidInputException error, and in every other region the call failed. They are now collected once, from us-east-1, when that region is part of the scan.ListAssets cannot list all assets at once: with its default filter it needs an asset model ID, and awsmap did not send one. Assets are now listed per asset model.ListAccessLogSubscriptions requires a resource identifier (a service network or a service). They are now listed per service network and per service.IAM and S3 also run their per-item calls concurrently, with a bounded pool.
awsmap 1.9.0 requires Python 3.10 or later. Python 3.9 is dropped.
No organization at hand? Generate a demo database and play with every command:
awsmap demo
awsmap query --db ~/.awsmap/demo.db -n admin-users
awsmap diff --db ~/.awsmap/demo.db --from 30d
The image is now multi-architecture: linux/amd64 and linux/arm64. Intel servers, Apple Silicon, Graviton. Docker picks the right one.
docker run --rm \
-v ~/.aws:/root/.aws:ro \
-v ~/.awsmap:/root/.awsmap \
tarekcheikh/awsmap org -p org-root --dry-run
| What | Command |
|---|---|
| Check access to every account | awsmap org -p org-root --dry-run |
| Scan the organization | awsmap org -p org-root |
| Scan one OU | awsmap org -p org-root --ou ou-xxxx-yyyyyyyy |
| Scan chosen accounts | awsmap org -p org-root --accounts 111122223333,444455556666 |
| Limit services and regions | awsmap org -p org-root -s ec2 -s s3 -r eu-west-1 |
| Finish an interrupted scan | awsmap org -p org-root --resume |
| Gentler on the network | awsmap org -p org-root -w 64 |
| Full role permissions | awsmap org -p org-root --no-session-policy |
| Resources per account and service | awsmap query -n resources-per-account-service |
| List partial accounts | awsmap query "SELECT account_id, incomplete_services FROM scans WHERE incomplete_services IS NOT NULL" |
pip install --upgrade awsmap
docker pull tarekcheikh/awsmap
Building and maintaining open-source tools takes time, and knowing that people use them is what keeps it going. If awsmap saved you time or gave you visibility into your AWS environment, a star on the GitHub repo helps others discover it and tells me this work matters.
And if you have an organization with a setup I did not think of, open an issue. The real-world cases are what made this release.
GitHub: github.com/TocConsulting/awsmap
PyPI: pypi.org/project/awsmap
Docker: hub.docker.com/r/tarekcheikh/awsmap
Documentation on this site: awsmap
This article is just the start. Get the full picture with our free whitepaper - 8 chapters covering IAM, S3, VPC, monitoring, agentic AI security, compliance, and a prioritized action plan with 50+ CLI commands.
Toc Consulting: AWS Security & Cloud Architecture
Our team helps engineering teams secure and architect AWS the right way: assessment in week one, a prioritized action plan in week two.
A few weeks ago I published awsmap. Scan your AWS account. 140 services. One command. This is v1.5.0 with SQLite storage, natural language queries, pre-built security audits, and multi-account support.
A CLI tool that scans 140+ AWS services across all regions in about 130 seconds, generating HTML, JSON, or CSV inventory reports for audits, cost analysis, and security reviews.
A CLI tool that generates cryptographically secure passwords with compliance templates, .env file generation, secret manager integration, and TOTP support.