Developer Tools8 min read

    awsmap v1.9.0: Scan Your Entire AWS Organization in One Command

    Tarek Cheikh

    Founder & AWS Cloud Architect

    awsmap 1.9.0: a map of an AWS Organization tree scanned from one point, with a few accounts flagged with warning marks

    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.

    The Milestone

    Five releases, one idea: your AWS inventory should be a living dataset, not a one-shot report.

    • v1.5.0: every scan goes to SQLite. Query it with SQL or plain English.
    • v1.6.0: an indexed is_current column replaces heavy subqueries in the queries.
    • v1.7.0: awsmap diff. See what was added, removed or modified between two scans.
    • v1.8.0: awsmap waste, awsmap tags, and --scan to query any point in your history.
    • v1.9.0: 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.

    awsmap org

    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.

    Why It Is Fast

    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.

    • The same service and region is collected for several accounts in one batch, over one HTTPS connection.
    • Each request is signed for the account being processed. By design, a call made outside an account context raises an error instead of running as another identity.
    • A few worker processes share one queue. A slow service or region holds one thread, not the whole scan.
    • Services that do not exist in a region are skipped. The regions enabled in each member account are used; if awsmap cannot read that list for an account, it falls back to the management account's list.

    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.

    Read-Only by Design

    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:

    • The management account is read with your own profile credentials, with no session policy.
    • The fallback matters. If STS refuses the managed-policy attachment (a permission boundary or an SCP, for example), awsmap retries with the inline policy alone, and it does so without a warning: the run still reports the session policy as active. That inline policy allows every action except the ten denied ones, so with an administrator role the session is no longer read-only.
    • With -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.

    It Tells You What It Could Not See

    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.

    Four Collector Fixes

    While preparing this release I fixed four collectors that were returning incomplete or wrong data:

    • Lambda tags were read from the wrong account. Tags were fetched with a session rebuilt from the profile name. Under assumed-role or environment credentials, that is the wrong identity, so the tags could be read with the wrong credentials. Tags now come from the same client that listed the functions. As a bonus, the aiobotocore dependency is gone.
    • Lightsail distributions and domains. The Lightsail API documents that the domain and distribution APIs are only available in 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.
    • IoT SiteWise assets were never returned. 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.
    • VPC Lattice access log subscriptions were never returned. 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.

    One Breaking Change

    awsmap 1.9.0 requires Python 3.10 or later. Python 3.9 is dropped.

    Try It Without an AWS Account

    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

    Docker

    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

    Quick Reference

    WhatCommand
    Check access to every accountawsmap org -p org-root --dry-run
    Scan the organizationawsmap org -p org-root
    Scan one OUawsmap org -p org-root --ou ou-xxxx-yyyyyyyy
    Scan chosen accountsawsmap org -p org-root --accounts 111122223333,444455556666
    Limit services and regionsawsmap org -p org-root -s ec2 -s s3 -r eu-west-1
    Finish an interrupted scanawsmap org -p org-root --resume
    Gentler on the networkawsmap org -p org-root -w 64
    Full role permissionsawsmap org -p org-root --no-session-policy
    Resources per account and serviceawsmap query -n resources-per-account-service
    List partial accountsawsmap query "SELECT account_id, incomplete_services FROM scans WHERE incomplete_services IS NOT NULL"

    Upgrade

    pip install --upgrade awsmap
    docker pull tarekcheikh/awsmap

    If awsmap Helped You, a Star Goes a Long Way

    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.

    Links

    GitHub: github.com/TocConsulting/awsmap
    PyPI: pypi.org/project/awsmap
    Docker: hub.docker.com/r/tarekcheikh/awsmap
    Documentation on this site: awsmap

    Go Deeper: The State of AWS Security 2026

    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.

    AWSCLI ToolsAWS OrganizationsCloud InventoryMulti-AccountOpen Source

    Toc Consulting: AWS Security & Cloud Architecture

    Want expert help with Developer Tools?

    Our team helps engineering teams secure and architect AWS the right way: assessment in week one, a prioritized action plan in week two.