Skip to content

Commit 06319b7

Browse files
[Rule Tuning] Add KEEP Command to all ES|QL Rules (#4146)
* updating ES|QL rules to include KEEP command * fixed some ES|QL rules with typos; added validation for KEEP command * fixed ES|QL errors from missing fields * fixed flake errors * updated date * added best practices to hunt docs
1 parent 4edef2e commit 06319b7

File tree

27 files changed

+81
-28
lines changed

27 files changed

+81
-28
lines changed

detection_rules/rule.py

+7
Original file line numberDiff line numberDiff line change
@@ -926,6 +926,13 @@ def validates_esql_data(self, data, **kwargs):
926926
f" Add 'metadata _id, _version, _index' to the from command or add an aggregate function."
927927
)
928928

929+
# Enforce KEEP command for ESQL rules
930+
if '| keep' not in query_lower:
931+
raise ValidationError(
932+
f"Rule: {data['name']} does not contain a 'keep' command ->"
933+
f" Add a 'keep' command to the query."
934+
)
935+
929936

930937
@dataclass(frozen=True)
931938
class ThreatMatchRuleData(QueryRuleData):

hunting/README.md

+6
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ Otherwise, the names do not require the integration, since it is already annotat
4444
- **mitre**: Reference to applicable MITRE ATT&CK tactics or techniques that the rule addresses, enhancing the contextual understanding of its security implications.
4545
- **references**: Links to external documents, research papers, or websites that provide additional information or validation for the detection logic.
4646

47+
#### Query Best Practices
48+
* Use `KEEP` command to select specific fields that are relevant or necessary for `STATS` command
49+
* Use `LIMIT` command to limit the number of results, depending on expected result volume
50+
* Filter as much as possible in `WHERE` command to reduce events needed to be processed
51+
* For `FROM` command for index patterns, be as specific as possible to reduce potential event matches that are irrelevant
52+
4753
### Field Usage
4854
Use standardized fields where possible to ensure that queries are compatible across different data environments and sources.
4955

rules/cross-platform/execution_potential_widespread_malware_infection.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/08"
33
maturity = "production"
4-
updated_date = "2024/05/08"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully"
66
min_stack_version = "8.13.0"
77

@@ -34,6 +34,7 @@ type = "esql"
3434
query = '''
3535
from logs-endpoint.alerts-*
3636
| where event.code in ("malicious_file", "memory_signature", "shellcode_thread") and rule.name is not null
37+
| keep host.id, rule.name, event.code
3738
| stats hosts = count_distinct(host.id) by rule.name, event.code
3839
| where hosts >= 3
3940
'''

rules/integrations/aws/discovery_ec2_multi_region_describe_instances.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/08/26"
33
maturity = "production"
4-
updated_date = "2024/08/26"
4+
updated_date = "2024/10/09"
55

66
[rule]
77
author = ["Elastic"]
@@ -41,6 +41,9 @@ from logs-aws.cloudtrail-*
4141
// truncate the timestamp to a 30-second window
4242
| eval target_time_window = DATE_TRUNC(30 seconds, @timestamp)
4343
44+
// keep only the relevant fields
45+
| keep target_time_window, aws.cloudtrail.user_identity.arn, cloud.region
46+
4447
// count the number of unique regions and total API calls within the 30-second window
4548
| stats region_count = count_distinct(cloud.region), window_count = count(*) by target_time_window, aws.cloudtrail.user_identity.arn
4649

rules/integrations/aws/discovery_servicequotas_multi_region_service_quota_requests.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/08/26"
33
maturity = "production"
4-
updated_date = "2024/10/02"
4+
updated_date = "2024/10/09"
55

66
[rule]
77
author = ["Elastic"]
@@ -48,6 +48,9 @@ from logs-aws.cloudtrail-*
4848
// filter for EC2 service quota L-1216C47A (vCPU on-demand instances)
4949
| where service_code == "ec2" and quota_code == "L-1216C47A"
5050
51+
// keep only the relevant fields
52+
| keep target_time_window, aws.cloudtrail.user_identity.arn, cloud.region, service_code, quota_code
53+
5154
// count the number of unique regions and total API calls within the 30-second window
5255
| stats region_count = count_distinct(cloud.region), window_count = count(*) by target_time_window, aws.cloudtrail.user_identity.arn
5356

rules/integrations/aws/impact_aws_s3_bucket_enumeration_or_brute_force.toml

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/01"
33
maturity = "production"
4-
updated_date = "2024/07/06"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully"
66
min_stack_version = "8.13.0"
77

@@ -89,6 +89,8 @@ type = "esql"
8989
query = '''
9090
from logs-aws.cloudtrail*
9191
| where event.provider == "s3.amazonaws.com" and aws.cloudtrail.error_code == "AccessDenied"
92+
// keep only relevant fields
93+
| keep tls.client.server_name, source.address, cloud.account.id
9294
| stats failed_requests = count(*) by tls.client.server_name, source.address, cloud.account.id
9395
// can modify the failed request count or tweak time window to fit environment
9496
// can add `not cloud.account.id in (KNOWN)` or specify in exceptions

rules/integrations/aws/impact_s3_bucket_object_uploaded_with_ransom_extension.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ integration = ["aws"]
44
maturity = "production"
55
min_stack_comments = "AWS integration breaking changes, bumping version to ^2.0.0"
66
min_stack_version = "8.13.0"
7-
updated_date = "2024/07/01"
7+
updated_date = "2024/10/09"
88

99
[rule]
1010
author = ["Elastic"]
@@ -98,6 +98,9 @@ from logs-aws.cloudtrail-*
9898
| where object_name rlike "(.*)(ransom|lock|crypt|enc|readme|how_to_decrypt|decrypt_instructions|recovery|datarescue)(.*)"
9999
and not object_name rlike "(.*)(AWSLogs|CloudTrail|access-logs)(.*)"
100100
101+
// keep relevant fields
102+
| keep tls.client.server_name, aws.cloudtrail.user_identity.arn, object_name
103+
101104
// aggregate by S3 bucket, resource and object name
102105
| stats note_upload_count = count(*) by tls.client.server_name, aws.cloudtrail.user_identity.arn, object_name
103106

rules/integrations/aws/impact_s3_object_encryption_with_external_key.toml

+4-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ integration = ["aws"]
44
maturity = "production"
55
min_stack_comments = "ES|QL rule type in technical preview as of 8.13"
66
min_stack_version = "8.13.0"
7-
updated_date = "2024/10/02"
7+
updated_date = "2024/10/09"
88

99
[rule]
1010
author = ["Elastic"]
@@ -95,8 +95,10 @@ from logs-aws.cloudtrail-* metadata _id, _version, _index
9595
// filter for s3 objects whose account id is different from the encryption key's account id
9696
// add exceptions based on key.account.id or keyId for known external accounts or encryption keys
9797
| where cloud.account.id != key.account.id
98-
'''
9998
99+
// keep relevant fields
100+
| keep @timestamp, aws.cloudtrail.user_identity.arn, cloud.account.id, event.action, target.bucketName, key.account.id, keyId, target.objectName
101+
'''
100102

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

rules/integrations/aws/initial_access_signin_console_login_no_mfa.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
creation_date = "2024/08/19"
33
integration = ['aws']
44
maturity = "production"
5-
updated_date = "2024/10/02"
5+
updated_date = "2024/10/09"
66
min_stack_comments = "ES|QL rule type in technical preview as of 8.13"
77
min_stack_version = "8.13.0"
88

@@ -44,6 +44,7 @@ from logs-aws.cloudtrail-* metadata _id, _version, _index
4444
and aws.cloudtrail.user_identity.type == "FederatedUser"
4545
| dissect aws.cloudtrail.additional_eventdata "{%{?mobile_version_key}=%{mobile_version}, %{?mfa_used_key}=%{mfa_used}}"
4646
| where mfa_used == "No"
47+
| keep @timestamp, event.action, aws.cloudtrail.event_type, aws.cloudtrail.user_identity.type
4748
'''
4849

4950
[[rule.threat]]

rules/integrations/aws/persistence_iam_user_created_access_keys_for_another_user.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
creation_date = "2024/06/13"
33
integration = ["aws"]
44
maturity = "production"
5-
updated_date = "2024/10/02"
5+
updated_date = "2024/10/09"
66
min_stack_comments = "ES|QL rule type in technical preview as of 8.13"
77
min_stack_version = "8.13.0"
88

@@ -104,6 +104,7 @@ from logs-aws.cloudtrail-* metadata _id, _version, _index
104104
and event.action == "CreateAccessKey"
105105
and event.outcome == "success"
106106
and user.name != user.target.name
107+
| keep @timestamp, event.provider, event.action, event.outcome, user.name, user.target.name
107108
'''
108109

109110

rules/integrations/aws/privilege_escalation_iam_administratoraccess_policy_attached_to_group.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
creation_date = "2024/05/31"
33
integration = ["aws"]
44
maturity = "production"
5-
updated_date = "2024/10/02"
5+
updated_date = "2024/10/09"
66
min_stack_comments = "ES|QL rule type in technical preview as of 8.13."
77
min_stack_version = "8.13.0"
88

@@ -104,6 +104,7 @@ from logs-aws.cloudtrail-* metadata _id, _version, _index
104104
| where event.provider == "iam.amazonaws.com" and event.action == "AttachGroupPolicy" and event.outcome == "success"
105105
| dissect aws.cloudtrail.request_parameters "{%{?policyArn}=%{?arn}:%{?aws}:%{?iam}::%{?aws}:%{?policy}/%{policyName},%{?groupName}=%{group.name}}"
106106
| where policyName == "AdministratorAccess"
107+
| keep @timestamp, event.provider, event.action, event.outcome, policyName, group.name
107108
'''
108109

109110

rules/integrations/aws/privilege_escalation_iam_administratoraccess_policy_attached_to_role.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
creation_date = "2024/05/31"
33
integration = ["aws"]
44
maturity = "production"
5-
updated_date = "2024/10/02"
5+
updated_date = "2024/10/09"
66
min_stack_comments = "ES|QL rule type in technical preview as of 8.13."
77
min_stack_version = "8.13.0"
88

@@ -103,6 +103,7 @@ from logs-aws.cloudtrail-* metadata _id, _version, _index
103103
| where event.provider == "iam.amazonaws.com" and event.action == "AttachRolePolicy" and event.outcome == "success"
104104
| dissect aws.cloudtrail.request_parameters "{%{?policyArn}=%{?arn}:%{?aws}:%{?iam}::%{?aws}:%{?policy}/%{policyName},%{?roleName}=%{role.name}}"
105105
| where policyName == "AdministratorAccess"
106+
| keep @timestamp, event.provider, event.action, event.outcome, policyName, role.name
106107
'''
107108

108109

rules/integrations/aws/privilege_escalation_iam_administratoraccess_policy_attached_to_user.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
creation_date = "2024/05/30"
33
integration = ["aws"]
44
maturity = "production"
5-
updated_date = "2024/10/02"
5+
updated_date = "2024/10/09"
66
min_stack_comments = "ES|QL rule type in technical preview as of 8.13."
77
min_stack_version = "8.13.0"
88

@@ -103,6 +103,7 @@ from logs-aws.cloudtrail-* metadata _id, _version, _index
103103
| where event.provider == "iam.amazonaws.com" and event.action == "AttachUserPolicy" and event.outcome == "success"
104104
| dissect aws.cloudtrail.request_parameters "{%{?policyArn}=%{?arn}:%{?aws}:%{?iam}::%{?aws}:%{?policy}/%{policyName},%{?userName}=%{target.userName}}"
105105
| where policyName == "AdministratorAccess"
106+
| keep @timestamp, event.provider, event.action, event.outcome, policyName, target.userName
106107
'''
107108

108109

rules/integrations/aws_bedrock/aws_bedrock_guardrails_multiple_violations_by_single_user.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/02"
33
maturity = "production"
4-
updated_date = "2024/09/27"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully; integration in tech preview"
66
min_stack_version = "8.13.0"
77

@@ -48,6 +48,7 @@ type = "esql"
4848
query = '''
4949
from logs-aws_bedrock.invocation-*
5050
| where gen_ai.compliance.violation_detected
51+
| keep user.id, gen_ai.request.model.id, cloud.account.id
5152
| stats violations = count(*) by user.id, gen_ai.request.model.id, cloud.account.id
5253
| where violations > 1
5354
| sort violations desc

rules/integrations/aws_bedrock/aws_bedrock_guardrails_multiple_violations_in_single_request.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/02"
33
maturity = "production"
4-
updated_date = "2024/05/02"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully; integration in tech preview"
66
min_stack_version = "8.13.0"
77

@@ -50,6 +50,7 @@ from logs-aws_bedrock.invocation-*
5050
| where gen_ai.policy.action == "BLOCKED"
5151
| eval policy_violations = mv_count(gen_ai.policy.name)
5252
| where policy_violations > 1
53+
| keep gen_ai.policy.action, policy_violations, user.id, gen_ai.request.model.id, cloud.account.id, user.id
5354
| stats total_unique_request_violations = count(*) by policy_violations, user.id, gen_ai.request.model.id, cloud.account.id
5455
| sort total_unique_request_violations desc
5556
'''

rules/integrations/aws_bedrock/aws_bedrock_high_confidence_misconduct_blocks_detected.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/05"
33
maturity = "production"
4-
updated_date = "2024/05/05"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully; integration in tech preview"
66
min_stack_version = "8.13.0"
77

@@ -46,6 +46,7 @@ type = "esql"
4646
query = '''
4747
from logs-aws_bedrock.invocation-*
4848
| where gen_ai.policy.confidence == "HIGH" and gen_ai.policy.action == "BLOCKED" and gen_ai.compliance.violation_code == "MISCONDUCT"
49+
| keep gen_ai.policy.confidence, gen_ai.policy.action, gen_ai.compliance.violation_code, user.id
4950
| stats high_confidence_blocks = count() by user.id
5051
| where high_confidence_blocks > 5
5152
| sort high_confidence_blocks desc

rules/integrations/aws_bedrock/aws_bedrock_high_resource_consumption_detection.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/04"
33
maturity = "production"
4-
updated_date = "2024/05/04"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully; integration in tech preview"
66
min_stack_version = "8.13.0"
77

@@ -46,6 +46,7 @@ type = "esql"
4646

4747
query = '''
4848
from logs-aws_bedrock.invocation-*
49+
| keep user.id, gen_ai.usage.prompt_tokens, gen_ai.usage.completion_tokens
4950
| stats max_tokens = max(gen_ai.usage.prompt_tokens),
5051
total_requests = count(*),
5152
avg_response_size = avg(gen_ai.usage.completion_tokens)

rules/integrations/aws_bedrock/aws_bedrock_multiple_attempts_to_use_denied_models_by_user.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
[metadata]
22
creation_date = "2024/05/02"
33
maturity = "production"
4-
updated_date = "2024/05/02"
4+
updated_date = "2024/10/09"
55
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully; integration in tech preview"
66
min_stack_version = "8.13.0"
77

@@ -48,6 +48,7 @@ type = "esql"
4848
query = '''
4949
from logs-aws_bedrock.invocation-*
5050
| where gen_ai.response.error_code == "AccessDeniedException"
51+
| keep user.id, gen_ai.request.model.id, cloud.account.id, gen_ai.response.error_code
5152
| stats total_denials = count(*) by user.id, gen_ai.request.model.id, cloud.account.id
5253
| where total_denials > 3
5354
| sort total_denials desc

rules/integrations/aws_bedrock/aws_bedrock_multiple_validation_exception_errors_by_single_user.toml

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22
creation_date = "2024/09/11"
33
integration = ["aws_bedrock"]
44
maturity = "production"
5-
updated_date = "2024/09/11"
5+
updated_date = "2024/10/09"
66
min_stack_comments = "ES|QL rule type is still in technical preview as of 8.13, however this rule was tested successfully; integration in tech preview"
77
min_stack_version = "8.13.0"
88

99
[rule]
1010
author = ["Elastic"]
1111
description = """
12-
Identifies multiple validation exeception errors within AWS Bedrock. Validation errors occur when you run the InvokeModel or
13-
InvokeModelWithResponseStream APIs on a foundation model that uses an incorrect inference parameter or corresponding value.
12+
Identifies multiple validation exeception errors within AWS Bedrock. Validation errors occur when you run the InvokeModel or
13+
InvokeModelWithResponseStream APIs on a foundation model that uses an incorrect inference parameter or corresponding value.
1414
These errors also occur when you use an inference parameter for one model with a model that doesn't have the same API parameter.
1515
This could indicate attempts to bypass limitations of other approved models, or to force an impact on the environment by incurring
1616
exhorbitant costs.
@@ -54,6 +54,7 @@ from logs-aws_bedrock.invocation-*
5454
// truncate the timestamp to a 1-minute window
5555
| eval target_time_window = DATE_TRUNC(1 minutes, @timestamp)
5656
| where gen_ai.response.error_code == "ValidationException"
57+
| keep user.id, gen_ai.request.model.id, cloud.account.id, gen_ai.response.error_code, target_time_window
5758
// count the number of users causing validation errors within a 1 minute window
5859
| stats total_denials = count(*) by target_time_window, user.id, cloud.account.id
5960
| where total_denials > 3

rules/integrations/azure/credential_access_entra_signin_brute_force_microsoft_365.toml

+5-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ integration = ["azure"]
44
maturity = "production"
55
min_stack_comments = "ES|QL not available until 8.13.0 in technical preview."
66
min_stack_version = "8.13.0"
7-
updated_date = "2024/09/06"
7+
updated_date = "2024/10/09"
88

99
[rule]
1010
author = ["Elastic"]
@@ -58,6 +58,10 @@ from logs-azure.signinlogs*
5858
and event.outcome != "success"
5959
// for tuning review azure.signinlogs.properties.status.error_code
6060
// https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
61+
62+
// keep only relevant fields
63+
| keep target_time_window, event.dataset, event.category, azure.signinlogs.properties.resource_display_name, azure.signinlogs.category, event.outcome, azure.signinlogs.properties.user_principal_name, source.ip
64+
6165
// count the number of login sources and failed login attempts
6266
| stats
6367
login_source_count = count(source.ip),

rules/integrations/azure/credential_access_entra_signin_brute_force_microsoft_365_repeat_source.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ integration = ["azure"]
44
maturity = "production"
55
min_stack_comments = "ES|QL not available until 8.13.0 in technical preview."
66
min_stack_version = "8.13.0"
7-
updated_date = "2024/09/06"
7+
updated_date = "2024/10/09"
88

99
[rule]
1010
author = ["Elastic"]
@@ -58,6 +58,9 @@ from logs-azure.signinlogs*
5858
// For tuning, review azure.signinlogs.properties.status.error_code
5959
// https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes
6060
61+
// keep only relevant fields
62+
| keep event.dataset, event.category, azure.signinlogs.properties.resource_display_name, azure.signinlogs.category, event.outcome, azure.signinlogs.properties.user_principal_name, source.ip
63+
6164
// Count the number of unique targets per source IP
6265
| stats
6366
target_count = count_distinct(azure.signinlogs.properties.user_principal_name) by source.ip

rules/integrations/o365/credential_access_microsoft_365_brute_force_user_account_attempt.toml

+4-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ integration = ["o365"]
44
maturity = "production"
55
min_stack_comments = "ES|QL not available until 8.13.0 in technical preview."
66
min_stack_version = "8.13.0"
7-
updated_date = "2024/09/25"
7+
updated_date = "2024/10/09"
88

99
[rule]
1010
author = ["Elastic", "Willem D'Haese", "Austin Songer"]
@@ -75,6 +75,9 @@ from logs-o365.audit-*
7575
// filters only for logins from user or application, ignoring oauth:token
7676
and to_lower(o365.audit.ExtendedProperties.RequestType) rlike "(.*)login(.*)"
7777
78+
// keep only relevant fields
79+
| keep event.provider, event.dataset, event.category, o365.audit.UserId, event.action, source.ip, o365.audit.LogonError, o365.audit.ExtendedProperties.RequestType, o365.audit.Target.Type, target_time_window
80+
7881
// count the number of login sources and failed login attempts
7982
| stats
8083
login_source_count = count(source.ip),

0 commit comments

Comments
 (0)