Skip to content

[Rule Tuning] ESQL Query Field Dynamic Field Standardization #4912

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 82 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
9e9214f
adjusted Potential Widespread Malware Infection Across Multiple Hosts
terrancedejesus Jul 16, 2025
7b5855a
adjusted Microsoft Azure or Mail Sign-in from a Suspicious Source
terrancedejesus Jul 16, 2025
7c66bb5
adjusted AWS EC2 Multi-Region DescribeInstances API Calls
terrancedejesus Jul 16, 2025
9ae8568
adjusted AWS Discovery API Calls via CLI from a Single Resource
terrancedejesus Jul 16, 2025
868ce2e
adjusted AWS Service Quotas Multi-Region Requests
terrancedejesus Jul 16, 2025
c321f3d
adjusted AWS EC2 EBS Snapshot Shared or Made Public
terrancedejesus Jul 16, 2025
24c5b12
adjusted AWS S3 Bucket Enumeration or Brute Force
terrancedejesus Jul 16, 2025
a86052f
adjusted AWS EC2 EBS Snapshot Access Removed
terrancedejesus Jul 16, 2025
32e8664
adjusted Potential AWS S3 Bucket Ransomware Note Uploaded
terrancedejesus Jul 16, 2025
019e153
adjusted AWS S3 Object Encryption Using External KMS Key
terrancedejesus Jul 16, 2025
2244489
adjusted AWS S3 Static Site JavaScript File Uploaded
terrancedejesus Jul 16, 2025
e175310
adjusted AWS Access Token Used from Multiple Addresses
terrancedejesus Jul 16, 2025
af2198d
adjusted AWS Signin Single Factor Console Login with Federated User
terrancedejesus Jul 16, 2025
fcc8d02
adjusted AWS IAM AdministratorAccess Policy Attached to Group
terrancedejesus Jul 16, 2025
4eeedb0
adjusted AWS IAM AdministratorAccess Policy Attached to Role
terrancedejesus Jul 16, 2025
0c817ff
adjusted AWS IAM AdministratorAccess Policy Attached to User
terrancedejesus Jul 16, 2025
7c7afcc
adjusted AWS Bedrock Invocations without Guardrails Detected by a Sin…
terrancedejesus Jul 16, 2025
229e206
adjusted AWS Bedrock Guardrails Detected Multiple Violations by a Sin…
terrancedejesus Jul 16, 2025
0642d25
adjusted AWS Bedrock Guardrails Detected Multiple Policy Violations W…
terrancedejesus Jul 16, 2025
3566242
adjusted Unusual High Confidence Content Filter Blocks Detected
terrancedejesus Jul 16, 2025
ba58831
adjusted Potential Abuse of Resources by High Token Count and Large R…
terrancedejesus Jul 16, 2025
a7f84d0
AWS Bedrock Detected Multiple Attempts to use Denied Models by a Sing…
terrancedejesus Jul 16, 2025
bd84ad4
Unusual High Denied Sensitive Information Policy Blocks Detected
terrancedejesus Jul 16, 2025
0e505a7
adjusted Unusual High Denied Topic Blocks Detected
terrancedejesus Jul 16, 2025
020b230
adjusted AWS Bedrock Detected Multiple Validation Exception Errors by…
terrancedejesus Jul 16, 2025
1686dc8
adjusted Unusual High Word Policy Blocks Detected
terrancedejesus Jul 16, 2025
17b5894
adjusted Microsoft Entra ID Concurrent Sign-Ins with Suspicious Prope…
terrancedejesus Jul 16, 2025
e5c02e5
adjusted Azure Entra MFA TOTP Brute Force Attempts
terrancedejesus Jul 16, 2025
cd8d6df
adjusted Microsoft Entra ID Sign-In Brute Force Activity
terrancedejesus Jul 16, 2025
d99dd39
adjusted Microsoft Entra ID Exccessive Account Lockouts Detected
terrancedejesus Jul 16, 2025
80ab978
adjusted Microsoft 365 Brute Force via Entra ID Sign-Ins
terrancedejesus Jul 16, 2025
effa807
deprecated Azure Entra Sign-in Brute Force Microsoft 365 Accounts by …
terrancedejesus Jul 16, 2025
407ce82
adjusted Microsoft Entra ID Session Reuse with Suspicious Graph Access
terrancedejesus Jul 16, 2025
fbd7808
adjusted Suspicious Microsoft OAuth Flow via Auth Broker to DRS
terrancedejesus Jul 16, 2025
92dad47
adjusted Potential Denial of Azure OpenAI ML Service
terrancedejesus Jul 16, 2025
c90f6b2
adjusted Azure OpenAI Insecure Output Handling
terrancedejesus Jul 16, 2025
662e8f8
adjusted Potential Azure OpenAI Model Theft
terrancedejesus Jul 16, 2025
2155267
adjusted M365 OneDrive Excessive File Downloads with OAuth Token
terrancedejesus Jul 16, 2025
9f29fac
adjusted Multiple Microsoft 365 User Account Lockouts in Short Time W…
terrancedejesus Jul 16, 2025
595ba2d
adjusted Potential Microsoft 365 User Account Brute Force
terrancedejesus Jul 16, 2025
c3071cf
adjusted Suspicious Microsoft 365 UserLoggedIn via OAuth Code
terrancedejesus Jul 16, 2025
7007e9d
adjusted Multiple Device Token Hashes for Single Okta Session
terrancedejesus Jul 16, 2025
0beadb8
adjusted Multiple Okta User Authentication Events with Client Address
terrancedejesus Jul 16, 2025
0306f27
adjusted Multiple Okta User Authentication Events with Same Device To…
terrancedejesus Jul 16, 2025
728282e
adjusted High Number of Okta Device Token Cookies Generated for Authe…
terrancedejesus Jul 16, 2025
08e6faa
adjusted Okta User Sessions Started from Different Geolocations
terrancedejesus Jul 16, 2025
f714d83
adjusted High Number of Egress Network Connections from Unusual Execu…
terrancedejesus Jul 16, 2025
21754e3
adjusted Unusual Base64 Encoding/Decoding Activity
terrancedejesus Jul 16, 2025
59a6533
adjusted Potential Port Scanning Activity from Compromised Host
terrancedejesus Jul 16, 2025
a6b62db
adjusted Potential Subnet Scanning Activity from Compromised Host
terrancedejesus Jul 16, 2025
136152f
adjusted Unusual File Transfer Utility Launched
terrancedejesus Jul 16, 2025
9003afe
adjusted Potential Malware-Driven SSH Brute Force Attempt
terrancedejesus Jul 16, 2025
caf9c63
adjusted Unusual Process Spawned from Web Server Parent
terrancedejesus Jul 16, 2025
8838461
adjusted Unusual Command Execution from Web Server Parent
terrancedejesus Jul 16, 2025
48d6541
adjusted Rare Connection to WebDAV Target
terrancedejesus Jul 16, 2025
6a80d6e
adjusted Potential PowerShell Obfuscation via Invalid Escape Sequences
terrancedejesus Jul 16, 2025
f389c42
adjusted Potential PowerShell Obfuscation via Backtick-Escaped Variab…
terrancedejesus Jul 16, 2025
37e013d
adjusted Unusual File Creation by Web Server
terrancedejesus Jul 16, 2025
fab91af
adjusted Potential PowerShell Obfuscation via High Special Character …
terrancedejesus Jul 16, 2025
759dbd8
adjusted Potential Malicious PowerShell Based on Alert Correlation
terrancedejesus Jul 16, 2025
c9d007d
adjusted Potential PowerShell Obfuscation via Character Array Reconst…
terrancedejesus Jul 16, 2025
78149bc
adjusted Potential PowerShell Obfuscation via String Reordering
terrancedejesus Jul 16, 2025
54f40ab
adjusted Potential PowerShell Obfuscation via String Concatenation
terrancedejesus Jul 16, 2025
9f76cb2
adjusted Potential PowerShell Obfuscation via Reverse Keywords
terrancedejesus Jul 16, 2025
6b0411b
adjusted PowerShell Obfuscation via Negative Index String Reversal
terrancedejesus Jul 16, 2025
ff2db16
adjusted Dynamic IEX Reconstruction via Method String Access
terrancedejesus Jul 16, 2025
b5d61a0
adjusted Potential Dynamic IEX Reconstruction via Environment Variables
terrancedejesus Jul 16, 2025
e77d6cc
adjusted Potential PowerShell Obfuscation via High Numeric Character …
terrancedejesus Jul 16, 2025
f2aac69
adjusted Potential PowerShell Obfuscation via Concatenated Dynamic Co…
terrancedejesus Jul 16, 2025
832b1a4
adjusted Rare Connection to WebDAV Target
terrancedejesus Jul 16, 2025
0f4046d
adjusted Potential PowerShell Obfuscation via Invalid Escape Sequences
terrancedejesus Jul 16, 2025
af6b79c
adjusted Potential PowerShell Obfuscation via Backtick-Escaped Variab…
terrancedejesus Jul 16, 2025
c6f1821
adjusted Potential PowerShell Obfuscation via Character Array Reconst…
terrancedejesus Jul 16, 2025
9e47f76
adjusted Potential PowerShell Obfuscation via High Special Character …
terrancedejesus Jul 16, 2025
9676b94
adjusted Potential PowerShell Obfuscation via Special Character Overuse
terrancedejesus Jul 16, 2025
dec8a06
adjusted Potential PowerShell Obfuscation via String Reordering
terrancedejesus Jul 16, 2025
9cb94d9
Merge branch 'main' into 4909-rule-tuning-esql-query-field-dynamic-fi…
terrancedejesus Jul 16, 2025
ec0f1ed
adjusted Suspicious Microsoft 365 UserLoggedIn via OAuth Code
terrancedejesus Jul 16, 2025
2819eb2
adjusted fields that were inconsistent
terrancedejesus Jul 16, 2025
3fd7b89
adjusted additional fields
terrancedejesus Jul 17, 2025
49f4f5f
adjusted esql to Esql
terrancedejesus Jul 17, 2025
23c1cd8
adjusted several rules for common field names
terrancedejesus Jul 18, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
[metadata]
creation_date = "2024/09/06"
deprecation_date = "2025/07/16"
integration = ["azure"]
maturity = "production"
updated_date = "2025/06/06"
maturity = "deprecated"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[metadata]
creation_date = "2024/05/08"
maturity = "production"
updated_date = "2025/03/20"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -67,8 +67,8 @@ query = '''
from logs-endpoint.alerts-*
| where event.code in ("malicious_file", "memory_signature", "shellcode_thread") and rule.name is not null
| keep host.id, rule.name, event.code
| stats hosts = count_distinct(host.id) by rule.name, event.code
| where hosts >= 3
| stats Esql.host.id.count_distinct = count_distinct(host.id) by rule.name, event.code
| where Esql.host.id.count_distinct >= 3
'''


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
creation_date = "2025/04/29"
integration = ["azure", "o365"]
maturity = "production"
updated_date = "2025/07/02"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -78,21 +78,47 @@ type = "esql"

query = '''
FROM logs-*, .alerts-security.*
// query runs every 1 hour looking for activities occured during last 8 hours to match on disparate events
// query runs every 1 hour looking for activities occurred during last 8 hours to match on disparate events
| where @timestamp > NOW() - 8 hours
// filter for Azure or M365 sign-in and External Alerts with source.ip not null
| where TO_IP(source.ip) is not null and (event.dataset in ("o365.audit", "azure.signinlogs") or kibana.alert.rule.name == "External Alerts") and
// exclude private IP ranges
not CIDR_MATCH(TO_IP(source.ip), "10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29", "192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24", "192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4", "100.64.0.0/10", "192.175.48.0/24","198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "240.0.0.0/4", "::1","FE80::/10", "FF00::/8")
| where TO_IP(source.ip) is not null
and (event.dataset in ("o365.audit", "azure.signinlogs") or kibana.alert.rule.name == "External Alerts")
and not CIDR_MATCH(
TO_IP(source.ip),
"10.0.0.0/8", "127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.0.0/29",
"192.0.0.8/32", "192.0.0.9/32", "192.0.0.10/32", "192.0.0.170/32", "192.0.0.171/32", "192.0.2.0/24",
"192.31.196.0/24", "192.52.193.0/24", "192.168.0.0/16", "192.88.99.0/24", "224.0.0.0/4",
"100.64.0.0/10", "192.175.48.0/24", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24",
"240.0.0.0/4", "::1", "FE80::/10", "FF00::/8"
)

// capture relevant raw fields
| keep source.ip, event.action, event.outcome, event.dataset, kibana.alert.rule.name, event.category
// split alerts to 3 buckets - M365 mail access, azure sign-in and network related external alerts like NGFW and IDS
| eval mail_access_src_ip = case(event.dataset == "o365.audit" and event.action == "MailItemsAccessed" and event.outcome == "success", TO_IP(source.ip), null),
azure_src_ip = case(event.dataset == "azure.signinlogs" and event.outcome == "success", TO_IP(source.ip), null),
network_alert_src_ip = case(kibana.alert.rule.name == "External Alerts" and not event.dataset in ("o365.audit", "azure.signinlogs"), TO_IP(source.ip), null)
// aggregated alerts count by bucket and by source.ip
| stats total_alerts = count(*), is_mail_access = COUNT_DISTINCT(mail_access_src_ip), is_azure = COUNT_DISTINCT(azure_src_ip), unique_dataset = COUNT_DISTINCT(event.dataset),is_network_alert = COUNT_DISTINCT(network_alert_src_ip), datasets = VALUES(event.dataset), rules = VALUES(kibana.alert.rule.name), cat = VALUES(event.category) by source_ip = TO_IP(source.ip)
// filter for cases where there is a successful sign-in to azure or m365 mail and the source.ip is reported by a network external alert.
| where is_network_alert > 0 and unique_dataset >= 2 and (is_mail_access > 0 or is_azure > 0) and total_alerts <= 100

// classify each source IP based on alert type
| eval
Esql.source.ip.mail_access.case = case(event.dataset == "o365.audit" and event.action == "MailItemsAccessed" and event.outcome == "success", TO_IP(source.ip), null),
Esql.source.ip.azure_signin.case = case(event.dataset == "azure.signinlogs" and event.outcome == "success", TO_IP(source.ip), null),
Esql.source.ip.network_alert.case = case(kibana.alert.rule.name == "External Alerts" and not event.dataset in ("o365.audit", "azure.signinlogs"), TO_IP(source.ip), null)

// aggregate by source IP
| stats
Esql.event.count = count(*),
Esql.source.ip.mail_access.case.count_distinct = COUNT_DISTINCT(Esql.source.ip.mail_access.case),
Esql.source.ip.azure_signin.case.count_distinct = COUNT_DISTINCT(Esql.source.ip.azure_signin.case),
Esql.source.ip.network_alert.case.count_distinct = COUNT_DISTINCT(Esql.source.ip.network_alert.case),
Esql.event.dataset.count_distinct = COUNT_DISTINCT(event.dataset),
Esql.event.dataset.values = VALUES(event.dataset),
Esql.kibana.alert.rule.name.values = VALUES(kibana.alert.rule.name),
Esql.event.category.values = VALUES(event.category)
by Esql.source.ip = TO_IP(source.ip)

// correlation condition
| where
Esql.source.ip.network_alert.case.count_distinct > 0
and Esql.event.dataset.count_distinct >= 2
and (Esql.source.ip.mail_access.case.count_distinct > 0 or Esql.source.ip.azure_signin.case.count_distinct > 0)
and Esql.event.count <= 100
'''


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
creation_date = "2024/08/26"
integration = ["aws"]
maturity = "production"
updated_date = "2025/01/10"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -86,25 +86,30 @@ timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail-*
FROM logs-aws.cloudtrail-*

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

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

// keep only the relevant fields
| keep target_time_window, aws.cloudtrail.user_identity.arn, cloud.region
// keep only the relevant raw fields
| keep Esql.time_window.date_trunc, aws.cloudtrail.user_identity.arn, cloud.region

// count the number of unique regions and total API calls within the 30-second window
| stats region_count = count_distinct(cloud.region), window_count = count(*) by target_time_window, aws.cloudtrail.user_identity.arn
| stats
Esql.cloud.region.count_distinct = COUNT_DISTINCT(cloud.region),
Esql.event.count = COUNT(*)
by Esql.time_window.date_trunc, aws.cloudtrail.user_identity.arn

// filter for resources making DescribeInstances API calls in more than 10 regions within the 30-second window
| where region_count >= 10 and window_count >= 10
| where Esql.cloud.region.count_distinct >= 10 and Esql.event.count >= 10

// sort the results by time windows in descending order
| sort target_time_window desc
// sort the results by time window in descending order
| sort Esql.time_window.date_trunc desc
'''

[rule.investigation_fields]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
creation_date = "2024/11/04"
integration = ["aws"]
maturity = "production"
updated_date = "2025/03/20"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -80,14 +80,14 @@ timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail*
FROM logs-aws.cloudtrail*

// create time window buckets of 10 seconds
| eval time_window = date_trunc(10 seconds, @timestamp)
| eval Esql.time_window.date_trunc = DATE_TRUNC(10 seconds, @timestamp)
| where
event.dataset == "aws.cloudtrail"

// filter on CloudTrail audit logs for IAM, EC2, and S3 events only
// filter on CloudTrail audit logs for IAM, EC2, S3, etc.
and event.provider in (
"iam.amazonaws.com",
"ec2.amazonaws.com",
Expand All @@ -97,8 +97,7 @@ from logs-aws.cloudtrail*
"dynamodb.amazonaws.com",
"kms.amazonaws.com",
"cloudfront.amazonaws.com",
"elasticloadbalancing.amazonaws.com",
"cloudfront.amazonaws.com"
"elasticloadbalancing.amazonaws.com"
)

// ignore AWS service actions
Expand All @@ -112,24 +111,29 @@ from logs-aws.cloudtrail*

// filter for Describe, Get, List, and Generate API calls
| where true in (
starts_with(event.action, "Describe"),
starts_with(event.action, "Get"),
starts_with(event.action, "List"),
starts_with(event.action, "Generate")
STARTS_WITH(event.action, "Describe"),
STARTS_WITH(event.action, "Get"),
STARTS_WITH(event.action, "List"),
STARTS_WITH(event.action, "Generate")
)

// extract owner, identity type, and actor from the ARN
| dissect aws.cloudtrail.user_identity.arn "%{}::%{owner}:%{identity_type}/%{actor}"
| where starts_with(actor, "AWSServiceRoleForConfig") != true
| keep @timestamp, time_window, event.action, aws.cloudtrail.user_identity.arn
| dissect aws.cloudtrail.user_identity.arn "%{}::%{Esql.aws.cloudtrail.user_identity.arn.owner}:%{Esql.aws.cloudtrail.user_identity.arn.type}/%{Esql.aws.cloudtrail.user_identity.arn.roles}"
| where STARTS_WITH(Esql.aws.cloudtrail.user_identity.arn.roles, "AWSServiceRoleForConfig") != true

// keep relevant fields (preserving ECS fields and computed time window)
| keep @timestamp, Esql.time_window.date_trunc, event.action, aws.cloudtrail.user_identity.arn

// count the number of unique API calls per time window and actor
| stats
// count the number of unique API calls per time window and actor
unique_api_count = count_distinct(event.action) by time_window, aws.cloudtrail.user_identity.arn
Esql.event.action.count_distinct = COUNT_DISTINCT(event.action)
by Esql.time_window.date_trunc, aws.cloudtrail.user_identity.arn

// filter for more than 5 unique API calls per time window
| where unique_api_count > 5
// filter for more than 5 unique API calls per 10s window
| where Esql.event.action.count_distinct > 5

// sort the results by the number of unique API calls in descending order
| sort unique_api_count desc
| sort Esql.event.action.count_distinct desc
'''


Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[metadata]
creation_date = "2024/08/26"
maturity = "production"
updated_date = "2025/01/15"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -35,31 +35,44 @@ timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail-*
FROM logs-aws.cloudtrail-*

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

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

// pre-process the request parameters to extract the service code and quota code
| dissect aws.cloudtrail.request_parameters "{%{?service_code_key}=%{service_code}, %{?quota_code_key}=%{quota_code}}"
// 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 service_code == "ec2" and quota_code == "L-1216C47A"
| 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 target_time_window, aws.cloudtrail.user_identity.arn, cloud.region, service_code, quota_code

// count the number of unique regions and total API calls within the 30-second window
| stats region_count = count_distinct(cloud.region), window_count = count(*) by target_time_window, aws.cloudtrail.user_identity.arn

// filter for resources making DescribeInstances API calls in more than 10 regions within the 30-second window
| where region_count >= 10 and window_count >= 10

// sort the results by time windows in descending order
| sort target_time_window desc
| 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

// 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(*)
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

// sort by time window descending
| sort Esql.time_window.date_trunc desc
'''
note = """## Triage and analysis

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
creation_date = "2024/04/16"
integration = ["aws"]
maturity = "production"
updated_date = "2025/06/02"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -80,11 +80,32 @@ timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail-* metadata _id, _version, _index
| where event.provider == "ec2.amazonaws.com" and event.action == "ModifySnapshotAttribute" and event.outcome == "success"
| dissect aws.cloudtrail.request_parameters "{%{?snapshotId}=%{snapshotId},%{?attributeType}=%{attributeType},%{?createVolumePermission}={%{operationType}={%{?items}=[{%{?userId}=%{userId}}]}}}"
| where operationType == "add" and cloud.account.id != userId
| keep @timestamp, aws.cloudtrail.user_identity.arn, cloud.account.id, event.action, snapshotId, attributeType, operationType, userId, source.address
FROM logs-aws.cloudtrail-* METADATA _id, _version, _index
| where
event.provider == "ec2.amazonaws.com"
and event.action == "ModifySnapshotAttribute"
and event.outcome == "success"

// Extract snapshotId, attribute type, operation type, and userId
| dissect aws.cloudtrail.request_parameters
"{%{?snapshotId}=%{Esql.aws.cloudtrail.request_parameters.snapshot.id},%{?attributeType}=%{Esql.aws.cloudtrail.request_parameters.attribute.type},%{?createVolumePermission}={%{Esql.aws.cloudtrail.request_parameters.operation.type}={%{?items}=[{%{?userId}=%{Esql.aws.cloudtrail.request_parameters.user.id}}]}}}"

// Check for snapshot permission added for another AWS account
| where
Esql.aws.cloudtrail.request_parameters.operation.type == "add"
and cloud.account.id != Esql.aws.cloudtrail.request_parameters.user.id

// Keep ECS and derived fields
| keep
@timestamp,
aws.cloudtrail.user_identity.arn,
cloud.account.id,
event.action,
Esql.aws.cloudtrail.request_parameters.snapshot.id,
Esql.aws.cloudtrail.request_parameters.attribute.type,
Esql.aws.cloudtrail.request_parameters.operation.type,
Esql.aws.cloudtrail.request_parameters.user.id,
source.ip
'''


Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[metadata]
creation_date = "2024/05/01"
maturity = "production"
updated_date = "2025/03/20"
updated_date = "2025/07/16"

[rule]
author = ["Elastic"]
Expand Down Expand Up @@ -85,14 +85,30 @@ timestamp_override = "event.ingested"
type = "esql"

query = '''
from logs-aws.cloudtrail*
| where event.provider == "s3.amazonaws.com" and aws.cloudtrail.error_code == "AccessDenied"
// keep only relevant fields
| keep tls.client.server_name, source.address, cloud.account.id
| stats failed_requests = count(*) by tls.client.server_name, source.address, cloud.account.id
// can modify the failed request count or tweak time window to fit environment
// can add `not cloud.account.id in (KNOWN)` or specify in exceptions
| where failed_requests > 40
FROM logs-aws.cloudtrail*

| where
event.provider == "s3.amazonaws.com"
and aws.cloudtrail.error_code == "AccessDenied"
and tls.client.server_name IS NOT NULL
and cloud.account.id IS NOT NULL

// Keep only relevant ECS fields
| keep
tls.client.server_name,
source.address,
cloud.account.id

// Count access denied requests per server_name, source, and account
| stats
Esql.event.count = COUNT(*)
by
tls.client.server_name,
source.address,
cloud.account.id

// Threshold: more than 40 denied requests
| where Esql.event.count > 40
'''


Expand Down
Loading