LoFP LoFP / organizations with mature multi-region operations may legitimately query ec2 service quotas across regions for capacity planning, automation, or compliance validation. infrastructure-as-code tooling, quota monitoring solutions, or centralized cloud governance platforms may also generate similar activity. validate the identity, purpose, and historical behavior of the calling principal before treating this activity as malicious.

Techniques

Sample rules

AWS Service Quotas Multi-Region GetServiceQuota Requests

Description

Identifies when a single AWS principal makes GetServiceQuota API calls for the EC2 service quota L-1216C47A, across more than 10 AWS regions within a 30-second window. This quota represents the vCPU limit for on-demand EC2 instances. Adversaries commonly enumerate this quota across regions to assess capacity for large-scale instance deployment, including cryptocurrency mining, malware hosting, or command-and-control infrastructure. This behavior may indicate cloud infrastructure discovery using compromised credentials or a compromised workload.

Detection logic

from logs-aws.cloudtrail-* METADATA _id, _version, _index

// filter for GetServiceQuota API calls
| where
  event.dataset == "aws.cloudtrail"
  and event.provider == "servicequotas.amazonaws.com"
  and event.action == "GetServiceQuota"

// truncate the timestamp to a 30-second window
| eval Esql.time_window_date_trunc = date_trunc(30 seconds, @timestamp)

// dissect request parameters to extract service and quota code
| dissect aws.cloudtrail.request_parameters "{%{?Esql.aws_cloudtrail_request_parameters_service_code_key}=%{Esql.aws_cloudtrail_request_parameters_service_code}, %{?quota_code_key}=%{Esql.aws_cloudtrail_request_parameters_quota_code}}"

// filter for EC2 service quota L-1216C47A (vCPU on-demand instances)
| where Esql.aws_cloudtrail_request_parameters_service_code == "ec2" and Esql.aws_cloudtrail_request_parameters_quota_code == "L-1216C47A"

// keep only the relevant fields
| keep
    Esql.time_window_date_trunc,
    aws.cloudtrail.user_identity.arn,
    cloud.region,
    Esql.aws_cloudtrail_request_parameters_service_code,
    Esql.aws_cloudtrail_request_parameters_quota_code,
    aws.cloudtrail.request_parameters,
    @timestamp, 
    aws.cloudtrail.user_identity.type, 
    aws.cloudtrail.user_identity.access_key_id, 
    source.ip, 
    cloud.account.id, 
    user_agent.original, 
    source.as.organization.name, 
    data_stream.namespace
    
// count the number of unique regions and total API calls within the time window
| stats
    Esql.cloud_region_count_distinct = count_distinct(cloud.region),
    Esql.event_count = count(*),
    Esql.aws_cloudtrail_request_parameters_values = VALUES(aws.cloudtrail.request_parameters),
    Esql.event_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.source_ip_values = VALUES(source.ip),
    Esql.cloud_account_id_values = VALUES(cloud.account.id),
    Esql.user_agent_original_values = VALUES(user_agent.original),
    Esql.source_as_organization_name_values = VALUES(source.as.organization.name),
    Esql.cloud_region_values = VALUES(cloud.region),
    Esql.data_stream_namespace_values = VALUES(data_stream.namespace)
  by Esql.time_window_date_trunc, aws.cloudtrail.user_identity.arn

// filter for API calls in more than 10 regions within the 30-second window
| where
  Esql.cloud_region_count_distinct >= 10
  and Esql.event_count >= 10