LoFP LoFP / legitimate security scanners, cspm products, compliance jobs, and inventory automation may call the same read-only bucket apis across many buckets quickly. verify the principal arn, source ip, user agent, and schedule against known approved tooling before treating the activity as malicious.

Techniques

Sample rules

AWS S3 Rapid Bucket Posture API Calls from a Single Principal

Description

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.

Detection logic

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