Loading

AWS S3 Rapid Bucket Posture API Calls from a Single Principal

Identifies when the same AWS principal, from the same source IP, successfully invokes read-only S3 control-plane APIs that reveal bucket posture across many buckets in a short period. This pattern can indicate automated reconnaissance or security scanning, similar to CSPM tools and post-compromise enumeration. The rule excludes AWS service principals, requires programmatic-style sessions (not Management Console credentials), and requires populated resource and identity fields so nulls do not skew cardinality.

Rule type: esql
Rule indices:

Rule Severity: low
Risk Score: 21
Runs every:
Searches indices from: now-6m
Maximum alerts per execution: 100
References:

Tags:

  • Domain: Cloud
  • Data Source: AWS
  • Data Source: Amazon Web Services
  • Data Source: AWS S3
  • Data Source: AWS CloudTrail
  • Use Case: Threat Detection
  • Tactic: Discovery
  • Tactic: Collection
  • Resources: Investigation Guide

Version: 1
Rule authors:

  • Elastic

Rule license: Elastic License v2

This rule detects when the same AWS principal (aws.cloudtrail.user_identity.arn), from the same source.ip, successfully invokes read-only S3 control-plane APIs that reveal bucket posture across more than 15 distinct aws.cloudtrail.resources.arn values within a 10-second window.

Security scanners, compliance tools, and post-compromise reconnaissance often walk many buckets quickly to map public access, policies, and versioning. Bursts of distinct buckets in seconds are less typical of one-off console administration or single-bucket troubleshooting. This could be indicative of reconnaissance as seen by threat actors like Team PCP.

Identify the actor and session context

  • Actor ARN (aws.cloudtrail.user_identity.arn): Determine which IAM user, role, or federated principal performed the reads. Confirm whether this identity is approved for broad S3 read or security auditing. Unusual types or unfamiliar roles may warrant deeper review.
  • Access key (Esql.aws_cloudtrail_user_identity_access_key_id_values): Identify which access key or temporary credential was used. Correlate with IAM last-used metadata for the key or role session.

Characterize the bucket sweep

  • Distinct bucket count (Esql.bucket_arn_count_distinct): Compare to normal baselines for this identity; values at or just above the threshold may still warrant review for new automation.
  • Bucket ARNs (Esql.aws_cloudtrail_resources_arn_values): Identify which buckets were touched. Prioritize buckets that store logs, backups, credentials, or regulated data. Search the same time range for write or policy-change APIs (PutBucket*, DeleteBucket*) on the same buckets.

Analyze source and client

  • Source IP (Esql.source_ip_values): Map to corporate egress, a known runner or bastion, an EC2 instance, or an unfamiliar ASN. Compare with VPC Flow Logs or proxy logs when available.
  • User agent (Esql.user_agent_original_values, Esql.user_agent_name_values): Identify the AWS CLI, Boto3, a specific scanner, or custom scripts. Unusual or minimal user agents may align with tooling and require investigation.

Correlate in time

  • Query CloudTrail for the same aws.cloudtrail.user_identity.arn and source.ip within approximately ±30 minutes for follow-on patterns: ListBuckets, GetObject, PutBucketPolicy, AssumeRole, or IAM changes.
  • Check for overlapping alerts related to credential access, unusual geolocation, or new external bucket policy grants.

Legitimate causes can include:

  • Security and compliance scanners (for example CSPM or assessment tools) using API credentials with s3:Get* permissions across many buckets.
  • Inventory or backup catalog tools that enumerate bucket metadata for reporting.
  • CI/CD or infrastructure-as-code validation jobs that verify bucket settings across environments.

Validate whether the principal is a documented service account, the IP belongs to known infrastructure, and the timing matches scheduled jobs. If behavior is expected, consider raising the distinct-bucket threshold, adding user_agent filters, or documenting exception identities.

Contain

  • If activity is unexpected, rotate or disable keys for the affected identity, revoke active role sessions where possible, and restrict the source IP at the network layer if it is not authorized.

Investigate

  • Export CloudTrail for the window around Esql.time_window_date_trunc and review all S3 and IAM events for the same actor.
  • Review IAM policies attached to the principal for excessive s3:Get* or s3:List* scope.

Harden

  • Enforce least privilege on S3 read APIs; use permission boundaries or service control policies where appropriate.
  • Ensure sensitive buckets are not unnecessarily reachable from the observed network context.
  • Document approved scanning accounts and tune the rule to reduce noise from those identities.
from logs-aws.cloudtrail-* metadata _id, _version, _index
| eval Esql.time_window_date_trunc = date_trunc(10 seconds, @timestamp)

| where
    event.dataset == "aws.cloudtrail"
    and event.provider == "s3.amazonaws.com"
    and event.outcome == "success"
    and event.action in (
      "GetBucketAcl",
      "GetBucketPublicAccessBlock",
      "GetBucketPolicy",
      "GetBucketPolicyStatus",
      "GetBucketVersioning"
    )
    and aws.cloudtrail.user_identity.type != "AWSService"
    and source.ip IS NOT NULL
    and aws.cloudtrail.resources.arn IS NOT NULL
    and aws.cloudtrail.user_identity.arn IS NOT NULL
    and aws.cloudtrail.session_credential_from_console IS NULL

| keep
    @timestamp,
    Esql.time_window_date_trunc,
    event.action,
    aws.cloudtrail.user_identity.arn,
    aws.cloudtrail.user_identity.type,
    aws.cloudtrail.user_identity.access_key_id,
    source.ip,
    aws.cloudtrail.resources.arn,
    cloud.account.id,
    cloud.region,
    user_agent.original,
    source.as.organization.name,
    data_stream.namespace

| stats
    Esql.bucket_arn_count_distinct = count_distinct(aws.cloudtrail.resources.arn),
    Esql.aws_cloudtrail_resources_arn_values = VALUES(aws.cloudtrail.resources.arn),
    Esql.event_action_values = VALUES(event.action),
    Esql.timestamp_values = VALUES(@timestamp),
    Esql.aws_cloudtrail_user_identity_type_values = VALUES(aws.cloudtrail.user_identity.type),
    Esql.aws_cloudtrail_user_identity_access_key_id_values = VALUES(aws.cloudtrail.user_identity.access_key_id),
    Esql.cloud_account_id_values = VALUES(cloud.account.id),
    Esql.cloud_region_values = VALUES(cloud.region),
    Esql.user_agent_original_values = VALUES(user_agent.original),
    Esql.source_as_organization_name_values = VALUES(source.as.organization.name),
    Esql.data_stream_namespace_values = VALUES(data_stream.namespace)
  by Esql.time_window_date_trunc, aws.cloudtrail.user_identity.arn, source.ip

| where Esql.bucket_arn_count_distinct > 15
		

Framework: MITRE ATT&CK

Framework: MITRE ATT&CK