23%
of cloud resources in enterprise environments are untagged and unattributed to a team (Flexera Cloud Report 2024)
3 clouds
covered: AWS, Azure, and GCP -- with equivalent commands for each major inventory category
1 week
target to complete a baseline asset inventory sprint before starting security remediation

Inheriting a cloud environment without documentation is one of the most common situations in security. An acquisition, a departing engineer, a startup that moved fast -- the result is the same: cloud accounts and resources that nobody can fully enumerate. You cannot secure what you cannot see. This guide is a one-week inventory sprint using CLI commands for AWS, Azure, and GCP to produce a complete asset inventory, identify internet-exposed resources, find orphaned accounts, and create the foundation for a security baseline.

Day 1: Account and Identity Enumeration

Start here. An account you don't know exists cannot be secured, and admin access you don't know about is your highest risk.

AWS: List all accounts in the Organization

# Requires OrganizationAccountAccessRole or equivalent in management account
aws organizations list-accounts \
  --query 'Accounts[].{ID:Id,Name:Name,Email:Email,Status:Status,Joined:JoinedTimestamp}' \
  --output table

# Find accounts not under any OU (potentially orphaned)
aws organizations list-accounts-for-parent \
  --parent-id $(aws organizations list-roots --query 'Roots[0].Id' --output text)

AWS: Find all IAM users with console access and admin permissions

# Generate and download credential report
aws iam generate-credential-report
aws iam get-credential-report --query 'Content' --output text | base64 -d > iam-report.csv

# Find users with admin-level policies
aws iam list-users --query 'Users[].UserName' --output text | \
  tr '\t' '\n' | while read user; do
    policies=$(aws iam list-attached-user-policies --user-name "$user" \
      --query 'AttachedPolicies[?PolicyName==`AdministratorAccess`].PolicyName' \
      --output text)
    [ -n "$policies" ] && echo "ADMIN USER: $user"
  done

# Find roles with admin access that can be assumed from outside the org
aws iam get-account-authorization-details \
  --filter Role \
  --query 'RoleDetailList[?contains(AssumeRolePolicyDocument.Statement[].Principal.AWS, `*`)].RoleName'

Azure: List all subscriptions in the tenant

az account list \
  --query '[].{Name:name,ID:id,State:state,TenantID:tenantId}' \
  -o table

# For each subscription, find all Global Administrators and privileged roles
az role assignment list --all \
  --query "[?roleDefinitionName=='Owner' || roleDefinitionName=='Contributor'].{Principal:principalName,Role:roleDefinitionName,Scope:scope}" \
  -o table

GCP: List all projects in the organization

gcloud projects list --format='table(projectId,name,createTime,lifecycleState)'

# Find all org-level IAM bindings with admin roles
gcloud organizations get-iam-policy $(gcloud organizations list --format='value(name)') \
  --flatten='bindings' \
  --filter='bindings.role:roles/owner OR bindings.role:roles/editor OR bindings.role:roles/iam.securityAdmin' \
  --format='table(bindings.role,bindings.members)'

Day 2: Internet-Exposed Resource Enumeration

These are your highest-priority findings. Anything internet-exposed is potentially reachable by a threat actor.

AWS: Find all EC2 instances with public IPs

aws ec2 describe-instances \
  --filters 'Name=instance-state-name,Values=running' \
  --query 'Reservations[].Instances[?PublicIpAddress!=null].{ID:InstanceId,Name:Tags[?Key==`Name`]|[0].Value,IP:PublicIpAddress,Type:InstanceType,Region:Placement.AvailabilityZone}' \
  --output table

AWS: Find S3 buckets with public access

# List all buckets
aws s3api list-buckets --query 'Buckets[].Name' --output text | tr '\t' '\n' > all-buckets.txt

# Check public access block configuration for each
while read bucket; do
  status=$(aws s3api get-public-access-block --bucket "$bucket" 2>/dev/null | \
    jq '.PublicAccessBlockConfiguration | to_entries | map(select(.value==false)) | length')
  [ "$status" -gt 0 ] && echo "PUBLIC ACCESS NOT FULLY BLOCKED: $bucket ($status settings disabled)"
done < all-buckets.txt

AWS: Find security groups with 0.0.0.0/0 ingress

for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  echo "--- Region: $region ---"
  aws ec2 describe-security-groups --region "$region" \
    --filters 'Name=ip-permission.cidr,Values=0.0.0.0/0' \
    --query 'SecurityGroups[].{ID:GroupId,Name:GroupName,Ports:IpPermissions[?IpRanges[?CidrIp==`0.0.0.0/0`]].{From:FromPort,To:ToPort}}' \
    --output json 2>/dev/null | jq -c '.[] | select(.Ports | length > 0)'
done

Azure: Find all public IPs and attached resources

az network public-ip list \
  --query '[].{Name:name,IP:ipAddress,RG:resourceGroup,AssociatedTo:ipConfiguration.id}' \
  -o table

# Find NSGs allowing inbound from any source on sensitive ports
az network nsg list --query '[].name' -o tsv | while read nsg; do
  rg=$(az network nsg show --name $nsg --query resourceGroup -o tsv 2>/dev/null)
  az network nsg show --name $nsg --resource-group $rg \
    --query "securityRules[?direction=='Inbound' && sourceAddressPrefix=='*' && access=='Allow'].{Name:name,Port:destinationPortRange}" \
    -o table 2>/dev/null
done

GCP: Find Compute instances with external IPs

gcloud compute instances list \
  --format='table(name,zone,machineType,networkInterfaces[0].accessConfigs[0].natIP:label=EXTERNAL_IP,status)' \
  --filter='networkInterfaces.accessConfigs.natIP:*'

# Find firewall rules allowing external SSH/RDP
gcloud compute firewall-rules list \
  --filter='sourceRanges:(0.0.0.0/0) AND allowed.ports:(22 OR 3389)' \
  --format='table(name,targetTags,allowed[0].ports,sourceRanges)'
Free daily briefing

Briefings like this, every morning before 9am.

Threat intel, active CVEs, and campaign alerts, distilled for practitioners. 50,000+ subscribers. No noise.

Day 3: Storage and Data Resource Enumeration

Find where data lives. These resources need data classification context applied.

AWS: Database inventory

# RDS instances
aws rds describe-db-instances \
  --query 'DBInstances[].{ID:DBInstanceIdentifier,Engine:Engine,Public:PubliclyAccessible,Encrypted:StorageEncrypted,MultiAZ:MultiAZ,Class:DBInstanceClass}' \
  --output table

# DynamoDB tables
aws dynamodb list-tables --query 'TableNames' --output text

# ElastiCache clusters
aws elasticache describe-cache-clusters \
  --query 'CacheClusters[].{ID:CacheClusterId,Engine:Engine,AtRestEncryption:AtRestEncryptionEnabled}'

# Secrets Manager -- what secrets exist
aws secretsmanager list-secrets \
  --query 'SecretList[].{Name:Name,LastRotated:LastRotatedDate,Desc:Description}' --output table

AWS: Find S3 buckets with no encryption

while read bucket; do
  enc=$(aws s3api get-bucket-encryption --bucket "$bucket" 2>&1)
  echo $enc | grep -q 'ServerSideEncryptionConfigurationNotFoundError' && \
    echo "NO ENCRYPTION: $bucket"
done < all-buckets.txt

Azure: Database and storage inventory

# All SQL databases
az sql db list --server YOUR_SERVER --resource-group YOUR_RG \
  --query '[].{Name:name,Status:status,Edition:edition,Size:maxSizeBytes}' -o table

# Storage accounts with public blob access
az storage account list \
  --query "[?allowBlobPublicAccess==true].{Name:name,RG:resourceGroup,PublicBlobAccess:allowBlobPublicAccess}" \
  -o table

# Key Vault inventory
az keyvault list --query '[].{Name:name,RG:resourceGroup,URI:properties.vaultUri}' -o table

GCP: Storage and database inventory

# Cloud Storage buckets
gsutil ls -p PROJECT_ID

# Check for public buckets
gsutil iam get gs://BUCKET_NAME | grep 'allUsers\|allAuthenticatedUsers'

# Cloud SQL instances
gcloud sql instances list --format='table(name,database_version,region,settings.tier,ipAddresses)'

# BigQuery datasets
bq ls --format=prettyjson --project_id PROJECT_ID

Day 4: Compute and Network Topology

Map the network architecture and identify unexpected compute resources.

AWS: Complete compute inventory across all regions

#!/bin/bash
# Save as: inventory-compute.sh
echo "Region,InstanceID,Name,State,Type,Public IP,Private IP,Launch Time"
for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  aws ec2 describe-instances --region "$region" \
    --query 'Reservations[].Instances[].{
      Region:`'"$region"'`,
      ID:InstanceId,
      Name:Tags[?Key==`Name`]|[0].Value,
      State:State.Name,
      Type:InstanceType,
      PublicIP:PublicIpAddress,
      PrivateIP:PrivateIpAddress,
      Launched:LaunchTime
    }' \
    --output text 2>/dev/null | tr '\t' ','
done

AWS: Find Lambda functions (often overlooked)

for region in $(aws ec2 describe-regions --query 'Regions[].RegionName' --output text); do
  count=$(aws lambda list-functions --region "$region" \
    --query 'length(Functions)' 2>/dev/null || echo 0)
  [ "$count" -gt 0 ] && echo "$region: $count Lambda functions"
done

# List all functions in a region with their IAM roles
aws lambda list-functions \
  --query 'Functions[].{Name:FunctionName,Runtime:Runtime,Role:Role,LastModified:LastModified}' \
  --output table

AWS: VPC and network topology

# List all VPCs and their CIDR ranges
aws ec2 describe-vpcs \
  --query 'Vpcs[].{ID:VpcId,CIDR:CidrBlock,Name:Tags[?Key==`Name`]|[0].Value,Default:IsDefault}' \
  --output table

# Find VPC peering connections (unexpected connectivity between accounts)
aws ec2 describe-vpc-peering-connections \
  --query 'VpcPeeringConnections[?Status.Code==`active`].{ID:VpcPeeringConnectionId,Requester:RequesterVpcInfo.VpcId,AccepterVpc:AccepterVpcInfo.VpcId,RequesterAccount:RequesterVpcInfo.OwnerId}' \
  --output table

# Find Transit Gateway attachments
aws ec2 describe-transit-gateway-attachments \
  --query 'TransitGatewayAttachments[].{TGW:TransitGatewayId,Resource:ResourceId,Type:ResourceType,State:State}' \
  --output table

Day 5: Produce the Asset Inventory and Security Baseline

Compile everything into a single inventory that becomes your working document.

Asset inventory structure:

asset-inventory-[DATE].xlsx / Google Sheet

Tab 1: Cloud Accounts
  Provider | Account ID | Account Name | Purpose | Primary Owner | Status

Tab 2: Identity and Access
  Account | Principal | Type | Admin? | Last Active | Action Needed

Tab 3: Internet-Exposed Resources (PRIORITY)
  Account | Resource Type | Resource ID | Public IP | Open Ports | Data Classification | Risk Level

Tab 4: Data Resources
  Account | Resource Type | Name | Encrypted | Public Access | Owner | Data Classification

Tab 5: Compute Inventory
  Account | Region | Resource Type | Name | State | Public IP | Owner | Last Modified

Tab 6: Orphan Candidates
  Account | Resource Type | Name | Last Activity | Cost/Month | Action (Claim by DATE or decommission)

Immediate priorities from the inventory (in order):

  1. Admin accounts with no named owner or no MFA
  2. Internet-exposed resources with no owner tag
  3. Storage buckets with public access containing unclassified data
  4. Databases with no encryption at rest
  5. Active resources in accounts nobody claims

Tagging enforcement (implement immediately for new resources):

# AWS: SCP to require tags on resource creation
# Attach to the root OU or all non-management accounts
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": ["ec2:RunInstances", "s3:CreateBucket", "rds:CreateDBInstance"],
      "Resource": "*",
      "Condition": {
        "Null": {
          "aws:RequestedRegion": "false"
        },
        "StringNotLike": {
          "aws:RequestTag/Owner": "*"
        }
      }
    }
  ]
}

For existing untagged resources -- batch tagging sprint:

# Find all untagged EC2 instances and prompt for owner
aws ec2 describe-instances \
  --query 'Reservations[].Instances[?!Tags || !contains(Tags[].Key, `Owner`)].{ID:InstanceId,State:State.Name}' \
  --output text | while read id state; do
    echo "$id ($state) -- Who owns this? Enter email:"
    read owner
    aws ec2 create-tags --resources "$id" --tags Key=Owner,Value="$owner"
  done

The bottom line

A cloud environment you don't understand is not a cloud environment you can secure. Spend one week on the inventory sprint before starting any remediation work -- the inventory will tell you where to focus and prevent you from wasting effort securing low-value resources while missing high-risk ones. The CLI commands in this guide take minutes to run; the real work is the human process of attributing resources to owners and making decisions about orphans. Start with accounts and identity, then internet exposure, then data, then compute. Everything else can wait.

Frequently asked questions

What should I inventory first when inheriting a cloud environment?

Start with accounts and identity, not with resources. An account you don't know exists cannot be secured. List all accounts in your AWS Organization, all subscriptions in your Azure tenant, and all projects in your GCP organization. Then enumerate IAM users, service accounts, and role bindings with admin-level access. Identity and access control is the blast radius of everything else -- if you find 200 misconfigured S3 buckets but miss a rogue admin account, the S3 work is irrelevant.

How do I find cloud resources that nobody knows about?

Query the billing data first -- every active resource generates cost. In AWS, use Cost Explorer filtered by service to find unexpected spend. Run aws resourcegroupstaggingapi get-resources with no tag filter to list every tagged and untagged resource. In Azure, use az resource list across all subscriptions. In GCP, use gcloud asset inventory search-all-resources at the organization level. Anything without an owner tag or that doesn't match known projects is a candidate for 'what is this?'

What is the fastest way to find internet-exposed resources?

In AWS: query for EC2 instances with public IPs (aws ec2 describe-instances --filters Name=network-interface.association.public-ip,Values=* ), S3 buckets with public access blocks disabled, and security groups with 0.0.0.0/0 inbound rules. In Azure: az network public-ip list to find all public IPs, then correlate to attached resources. In GCP: gcloud compute instances list --filter='networkInterfaces.accessConfigs.natIP:*' for VMs with public IPs. Tools like Shodan and Censys can also scan your known IP ranges to show what's actually reachable from the internet.

How do I handle resources that nobody knows the purpose of?

Do not delete unknown resources immediately. First: check billing history to see when the resource was created and by whom. Check CloudTrail/Activity Log for recent API calls to the resource. Check if any other resource references it (load balancers, security groups, IAM policies). If it has had no API activity in 90+ days and nothing references it: tag it 'orphan-candidate', notify the team via Slack/email, wait 14 days for anyone to claim it, then decommission. Deleting a running database that nobody thought was used is a bad day.

What tagging strategy should I implement in an inherited environment?

Minimum viable tag set: Environment (production/staging/dev/sandbox), Owner (team or individual email), CostCenter (billing attribution), and DataClassification (for resources storing data). Don't try to implement a perfect tagging taxonomy on day one -- enforce these four tags on new resources via policy (AWS SCPs, Azure Policy, GCP Organization Policy) and retroactively tag existing resources by working with team leads who know what each workload is.

How long does a cloud asset inventory sprint typically take?

For a single-cloud environment with 1-3 accounts/subscriptions: 2-3 days for a solo practitioner using the CLI commands in this guide. For a multi-cloud environment with 10+ accounts: 1-2 weeks with dedicated time. The bottleneck is usually not the CLI queries (which run in minutes) but the human work of attributing untagged resources to teams and owners. Block a full week for the inventory sprint and plan to do nothing else during it.

Sources & references

  1. Flexera State of the Cloud Report 2024
  2. AWS Well-Architected Framework Security Pillar
  3. CIS Controls
  4. NIST Cybersecurity Framework
  5. Wiz State of Cloud Security 2024

Free resources

25
Free download

Critical CVE Reference Card 2025–2026

25 actively exploited vulnerabilities with CVSS scores, exploit status, and patch availability. Print it, pin it, share it with your SOC team.

No spam. Unsubscribe anytime.

Free download

Ransomware Incident Response Playbook

Step-by-step 24-hour IR checklist covering detection, containment, eradication, and recovery. Built for SOC teams, IR leads, and CISOs.

No spam. Unsubscribe anytime.

Free newsletter

Get threat intel before your inbox does.

50,000+ security professionals read Decryption Digest for early warnings on zero-days, ransomware, and nation-state campaigns. Free, weekly, no spam.

Unsubscribe anytime. We never sell your data.

Eric Bang
Author

Founder & Cybersecurity Evangelist, Decryption Digest

Cybersecurity professional with expertise in threat intelligence, vulnerability research, and enterprise security. Covers zero-days, ransomware, and nation-state operations for 50,000+ security professionals weekly.

Free Brief

The Mythos Brief is free.

AI that finds 27-year-old zero-days. What it means for your security program.

Joins Decryption Digest. Unsubscribe anytime.

Related Questions — Answer Hub

Daily Briefing

Get briefings like this every morning

Actionable threat intelligence for working practitioners. Free. No spam. Trusted by 50,000+ SOC analysts, CISOs, and security engineers.

Unsubscribe anytime.

Mythos Brief

Anthropic's AI finds zero-days your scanners miss.