Back to Checks
aws

CloudTrail logs show no potential enumeration activity

cloudtrail_threat_detection_enumeration
Service: cloudtrailSeverity: critical
OfficialBuilt-inVerified
Categories:threat-detection
Resource Type:AwsCloudTrailTrail

CloudTrail activity is analyzed for AWS identities executing a broad mix of discovery APIs like List*, Describe*, and Get* within a recent time window.

An identity exceeding a configurable ratio of these actions indicates potential enumeration behavior by that principal.

Risk

Concentrated discovery activity signals reconnaissance with valid credentials. Adversaries can map assets and policies to enable privilege escalation, target data stores for exfiltration (confidentiality), and identify services to disrupt (availability), supporting stealthy lateral movement.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks cloudtrail_threat_detection_enumeration

Recommendation

Apply least privilege to limit List*/Describe*/Get* to necessary resources and roles; use separation of duties.

  • Enforce MFA and short-lived sessions
  • Use SCPs to curb unnecessary discovery
  • Baseline expected reads and alert on spikes as defense in depth

Remediation

CLI

aws iam update-access-key --user-name <USER_NAME> --access-key-id <ACCESS_KEY_ID> --status Inactive

Terraform

# Deny common enumeration APIs for a specific IAM user
resource "aws_iam_user_policy" "<example_resource_name>" {
  name = "deny-enumeration"
  user = "<example_user_name>"

  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [{
      Effect   = "Deny", # CRITICAL: blocks typical enumeration calls
      Action   = [
        "ec2:Describe*",   # CRITICAL
        "iam:List*",       # CRITICAL
        "s3:List*",        # CRITICAL
        "s3:Get*"          # CRITICAL
      ],
      Resource = "*"
    }]
  })
}

NativeIaC

# CloudFormation: deny common enumeration APIs for a specific IAM user
Resources:
  DenyEnumerationPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: deny-enumeration
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Deny  # CRITICAL: blocks typical enumeration calls
            Action:
              - ec2:Describe*   # CRITICAL: deny EC2 describe APIs
              - iam:List*       # CRITICAL: deny IAM list APIs
              - s3:List*        # CRITICAL: deny S3 list APIs
              - s3:Get*         # CRITICAL: deny S3 get APIs (e.g., GetBucketAcl)
            Resource: "*"
      Users:
        - "<example_resource_name>"  # CRITICAL: target the enumerating user

Other

1In AWS Console, go to IAM > Users and open the user shown in the alert (ARN in the finding)
2Select the Security credentials tab
3For each active Access key, click Deactivate to set status to Inactive
4If the activity came from an EC2 instance role: go to EC2 > Instances > select the instance > Security > IAM role > Detach IAM role
5Re-run the check to confirm no new enumeration events occur

Check Code

import json

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
    cloudtrail_client,
)

default_threat_detection_enumeration_actions = [
    "CreateIndex",
    "DescribeAccessEntry",
    "DescribeAccountAttributes",
    "DescribeAvailabilityZones",
    "DescribeBundleTasks",
    "DescribeCarrierGateways",
    "DescribeClientVpnRoutes",
    "DescribeCluster",
    "DescribeDhcpOptions",
    "DescribeFlowLogs",
    "DescribeImages",
    "DescribeInstanceAttribute",
    "DescribeInstanceInformation",
    "DescribeInstanceTypes",
    "DescribeInstances",
    "DescribeInstances",
    "DescribeKeyPairs",
    "DescribeLogGroups",
    "DescribeLogStreams",
    "DescribeOrganization",
    "DescribeRegions",
    "DescribeSecurityGroups",
    "DescribeSnapshotAttribute",
    "DescribeSnapshotTierStatus",
    "DescribeSubscriptionFilters",
    "DescribeTransitGatewayMulticastDomains",
    "DescribeVolumes",
    "DescribeVolumesModifications",
    "DescribeVpcEndpointConnectionNotifications",
    "DescribeVpcs",
    "GetAccount",
    "GetAccountAuthorizationDetails",
    "GetAccountSendingEnabled",
    "GetBucketAcl",
    "GetBucketLogging",
    "GetBucketPolicy",
    "GetBucketReplication",
    "GetBucketVersioning",
    "GetCallerIdentity",
    "GetCertificate",
    "GetConsoleScreenshot",
    "GetCostAndUsage",
    "GetDetector",
    "GetEbsDefaultKmsKeyId",
    "GetEbsEncryptionByDefault",
    "GetFindings",
    "GetFlowLogsIntegrationTemplate",
    "GetIdentityVerificationAttributes",
    "GetInstances",
    "GetIntrospectionSchema",
    "GetLaunchTemplateData",
    "GetLaunchTemplateData",
    "GetLogRecord",
    "GetParameters",
    "GetPolicyVersion",
    "GetPublicAccessBlock",
    "GetQueryResults",
    "GetRegions",
    "GetSMSAttributes",
    "GetSMSSandboxAccountStatus",
    "GetSendQuota",
    "GetTransitGatewayRouteTableAssociations",
    "GetUserPolicy",
    "HeadObject",
    "ListAccessKeys",
    "ListAccounts",
    "ListAllMyBuckets",
    "ListAssociatedAccessPolicies",
    "ListAttachedUserPolicies",
    "ListClusters",
    "ListDetectors",
    "ListDomains",
    "ListFindings",
    "ListHostedZones",
    "ListIPSets",
    "ListIdentities",
    "ListInstanceProfiles",
    "ListObjects",
    "ListOrganizationalUnitsForParent",
    "ListOriginationNumbers",
    "ListPolicyVersions",
    "ListResources",
    "ListRoles",
    "ListRoles",
    "ListRules",
    "ListServiceQuotas",
    "ListSubscriptions",
    "ListTargetsByRule",
    "ListTopics",
    "ListUsers",
    "LookupEvents",
    "Search",
]


class cloudtrail_threat_detection_enumeration(Check):
    def execute(self):
        findings = []
        threshold = cloudtrail_client.audit_config.get(
            "threat_detection_enumeration_threshold", 0.3
        )
        threat_detection_minutes = cloudtrail_client.audit_config.get(
            "threat_detection_enumeration_minutes", 1440
        )
        enumeration_actions = cloudtrail_client.audit_config.get(
            "threat_detection_enumeration_actions",
            default_threat_detection_enumeration_actions,
        )
        potential_enumeration = {}
        found_potential_enumeration = False
        multiregion_trail = None
        # Check if any trail is multi-region so we only need to check once
        for trail in cloudtrail_client.trails.values():
            if trail.is_multiregion:
                multiregion_trail = trail
                break
        trails_to_scan = (
            cloudtrail_client.trails.values()
            if not multiregion_trail
            else [multiregion_trail]
        )
        for trail in trails_to_scan:
            for event_name in enumeration_actions:
                for event_log in cloudtrail_client._lookup_events(
                    trail=trail,
                    event_name=event_name,
                    minutes=threat_detection_minutes,
                ):
                    event_log = json.loads(event_log["CloudTrailEvent"])
                    if (
                        "arn" in event_log["userIdentity"]
                    ):  # Ignore event logs without ARN since they are AWS services
                        if (
                            event_log["userIdentity"]["arn"],
                            event_log["userIdentity"]["type"],
                        ) not in potential_enumeration:
                            potential_enumeration[
                                (
                                    event_log["userIdentity"]["arn"],
                                    event_log["userIdentity"]["type"],
                                )
                            ] = set()
                        potential_enumeration[
                            (
                                event_log["userIdentity"]["arn"],
                                event_log["userIdentity"]["type"],
                            )
                        ].add(event_name)

        for aws_identity, actions in potential_enumeration.items():
            identity_threshold = round(len(actions) / len(enumeration_actions), 2)
            aws_identity_type = aws_identity[1]
            aws_identity_arn = aws_identity[0]
            if len(actions) / len(enumeration_actions) > threshold:
                found_potential_enumeration = True
                report = Check_Report_AWS(
                    metadata=self.metadata(), resource=cloudtrail_client.trails
                )
                report.region = cloudtrail_client.region
                report.resource_id = aws_identity_arn.split("/")[-1]
                report.resource_arn = aws_identity_arn
                report.status = "FAIL"
                report.status_extended = f"Potential enumeration attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with a threshold of {identity_threshold}."
                findings.append(report)
        if not found_potential_enumeration:
            report = Check_Report_AWS(
                metadata=self.metadata(), resource=cloudtrail_client.trails
            )
            report.region = cloudtrail_client.region
            report.resource_id = cloudtrail_client.audited_account
            report.resource_arn = cloudtrail_client._get_trail_arn_template(
                cloudtrail_client.region
            )
            report.status = "PASS"
            report.status_extended = "No potential enumeration attack detected."
            findings.append(report)
        return findings