Detection rules › Elastic

AWS Service Quotas Multi-Region GetServiceQuota Requests

Status
production
Severity
low
Time window
6m
Group by
Esql.time_window_date_trunc, aws.cloudtrail.user_identity.arn
Author
Elastic
Source
github.com/elastic/detection-rules

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.

MITRE ATT&CK coverage

Event coverage

Rule body elastic

[metadata]
creation_date = "2024/08/26"
maturity = "production"
updated_date = "2026/04/10"

[rule]
author = ["Elastic"]
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.
"""
false_positives = [
    """
    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.
    """,
]
from = "now-6m"
language = "esql"
license = "Elastic License v2"
name = "AWS Service Quotas Multi-Region GetServiceQuota Requests"
note = """## Triage and analysis

### Investigating AWS Service Quotas Multi-Region GetServiceQuota Requests

AWS Service Quotas define usage limits for AWS services and are commonly referenced during capacity planning or automation. However, adversaries frequently enumerate EC2 on-demand instance quotas across many regions to identify where they can rapidly deploy compute resources for malicious purposes such as cryptocurrency mining, botnet hosting, or malware staging. This rule detects unusually fast, multi-region enumeration of the EC2 on-demand vCPU quota (`L-1216C47A`), a pattern that is uncommon for normal administrative activity and strongly associated with cloud infrastructure discovery.

### Possible investigation steps

**Identify the actor**
- Review `aws.cloudtrail.user_identity.arn` and `aws.cloudtrail.user_identity.access_key_id` to determine whether the requests originated from an IAM user, role, or assumed role. Validate whether this principal is expected to perform quota discovery or capacity analysis across many regions.

**Evaluate the scope of discovery**
- Review the `cloud.region` values to determine which regions were queried and whether they align with regions normally used by your organization. Rapid enumeration of rarely used or disabled regions increases suspicion.

**Inspect request origin and tooling**
- Review `source.ip`, `source.as.organization.name`, and `user_agent.original` to determine whether the activity originated from a trusted corporate network, known cloud automation environment, or an unexpected hosting provider or VPN.
- Unexpected user agents or hosting providers may indicate compromised credentials or an attacker-controlled instance.

**Correlate with follow-on activity**
- Search for subsequent EC2-related actions such as `RunInstances`, `RequestSpotInstances`, `CreateLaunchTemplate`, or `ModifyInstanceAttribute` following the quota discovery.
- Review recent IAM activity for the same principal, including access key creation, role assumptions, or policy changes.

**Assess intent and risk**
- Determine whether this activity aligns with a known operational task (capacity planning, onboarding, automation testing), or whether it represents unexplained reconnaissance behavior.
- If the principal is newly created, rarely used, or exhibiting other anomalous behavior, treat this as high risk.

### False positive analysis
- Multi-region quota discovery may be legitimate in organizations with global deployments, centralized cloud governance, or automated capacity monitoring.
- Infrastructure-as-code pipelines, quota management tools, or internal cloud platforms may periodically enumerate quotas.

### Response and remediation
- If the activity is unauthorized or suspicious, immediately rotate or disable access keys associated with the principal and revoke active sessions.
- Review CloudTrail activity for evidence of follow-on abuse, particularly EC2 instance launches, network changes, or IAM modifications.
- Apply tighter IAM permissions to restrict access to Service Quotas APIs where not explicitly required.
- Enforce MFA on IAM users and consider conditional access controls (such as source IP or VPC restrictions) for sensitive roles.
- Notify security operations and cloud platform teams to assess potential impact and determine whether containment actions (such as SCP enforcement or account isolation) are required.
- Update detection coverage to monitor for EC2 provisioning attempts following quota discovery to catch resource abuse early.

### Additional information
- **[AWS IR Playbooks](https://github.com/aws-samples/aws-incident-response-playbooks/blob/c151b0dc091755fffd4d662a8f29e2f6794da52c/playbooks/)** 
- **[AWS Customer Playbook Framework](https://github.com/aws-samples/aws-customer-playbook-framework/tree/a8c7b313636b406a375952ac00b2d68e89a991f2/docs)** 
- **[AWS Knowledge Center – Security Best Practices](https://aws.amazon.com/premiumsupport/knowledge-center/security-best-practices/)**
"""
references = [
    "https://www.sentinelone.com/labs/exploring-fbot-python-based-malware-targeting-cloud-and-payment-services/",
    "https://docs.aws.amazon.com/servicequotas/2019-06-24/apireference/API_GetServiceQuota.html",
]
risk_score = 21
rule_id = "19be0164-63d2-11ef-8e38-f661ea17fbce"
severity = "low"
tags = [
    "Domain: Cloud",
    "Data Source: AWS",
    "Data Source: Amazon Web Services",
    "Data Source: AWS Service Quotas",
    "Use Case: Threat Detection",
    "Tactic: Discovery",
    "Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"

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

// filter for GetServiceQuota API calls
| where
  data_stream.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
'''


[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1526"
name = "Cloud Service Discovery"
reference = "https://attack.mitre.org/techniques/T1526/"

[[rule.threat.technique]]
id = "T1580"
name = "Cloud Infrastructure Discovery"
reference = "https://attack.mitre.org/techniques/T1580/"

[rule.threat.tactic]
id = "TA0007"
name = "Discovery"
reference = "https://attack.mitre.org/tactics/TA0007/"
[rule.investigation_fields]
field_names = [
    "Esql.cloud_region_count_distinct",
    "Esql.event_count",
    "Esql.time_window_date_trunc",
    "Esql.event_timestamp_values",
    "aws.cloudtrail.user_identity.arn",
    "Esql.aws_cloudtrail_user_identity_type_values",
    "Esql.aws_cloudtrail_user_identity_access_key_id_values",
    "Esql.source_ip_values",
    "Esql.source_as_organization_name_values",
    "Esql.user_agent_original_values",
    "Esql.cloud_account_id_values",
    "Esql.cloud_region_values",
    "Esql.data_stream_namespace_values",
]

Stages and Predicates

Stage 1: from

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

Stage 2: where

| where
  data_stream.dataset == "aws.cloudtrail"
  and event.provider == "servicequotas.amazonaws.com"
  and event.action == "GetServiceQuota"

Stage 3: eval

| eval Esql.time_window_date_trunc = date_trunc(30 seconds, @timestamp)

Stage 4: dissect

| 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}}"

Stage 5: where

| where Esql.aws_cloudtrail_request_parameters_service_code == "ec2" and Esql.aws_cloudtrail_request_parameters_quota_code == "L-1216C47A"

Stage 6: keep

| 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

Stage 7: stats

| 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

Stage 8: where

| where
  Esql.cloud_region_count_distinct >= 10
  and Esql.event_count >= 10

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Esql.cloud_region_count_distinctSTATS Esql.cloud_region_count_distinct = count_distinct(cloud.region)
Esql.event_countSTATS Esql.event_count = count(*)
Esql.aws_cloudtrail_request_parameters_valuesSTATS Esql.aws_cloudtrail_request_parameters_values = VALUES(aws.cloudtrail.request_parameters)
Esql.event_timestamp_valuesSTATS Esql.event_timestamp_values = VALUES(@timestamp)
Esql.aws_cloudtrail_user_identity_type_valuesSTATS Esql.aws_cloudtrail_user_identity_type_values = VALUES(aws.cloudtrail.user_identity.type)
Esql.aws_cloudtrail_user_identity_access_key_id_valuesSTATS Esql.aws_cloudtrail_user_identity_access_key_id_values = VALUES(aws.cloudtrail.user_identity.access_key_id)
Esql.source_ip_valuesSTATS Esql.source_ip_values = VALUES(source.ip)
Esql.cloud_account_id_valuesSTATS Esql.cloud_account_id_values = VALUES(cloud.account.id)
Esql.user_agent_original_valuesSTATS Esql.user_agent_original_values = VALUES(user_agent.original)
Esql.source_as_organization_name_valuesSTATS Esql.source_as_organization_name_values = VALUES(source.as.organization.name)
Esql.cloud_region_valuesSTATS Esql.cloud_region_values = VALUES(cloud.region)
Esql.data_stream_namespace_valuesSTATS Esql.data_stream_namespace_values = VALUES(data_stream.namespace)
Esql.time_window_date_truncSTATS BY
aws.cloudtrail.user_identity.arnSTATS BY