[Rule Tuning] Microsoft Azure or Mail Sign-in from a Suspicious Source (#4946)
* change indices in ESQL query * adjusted rule name
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
creation_date = "2025/04/29"
|
||||
integration = ["azure", "o365"]
|
||||
maturity = "production"
|
||||
updated_date = "2025/07/02"
|
||||
updated_date = "2025/07/30"
|
||||
|
||||
[rule]
|
||||
author = ["Elastic"]
|
||||
@@ -19,10 +19,10 @@ false_positives = [
|
||||
from = "now-60m"
|
||||
language = "esql"
|
||||
license = "Elastic License v2"
|
||||
name = "Microsoft Azure or Mail Sign-in from a Suspicious Source"
|
||||
name = "Microsoft 365 or Entra ID Sign-in from a Suspicious Source"
|
||||
note = """## Triage and analysis
|
||||
|
||||
### Investigating Microsoft Azure or Mail Sign-in from a Suspicious Source
|
||||
### Investigating Microsoft 365 or Entra ID Sign-in from a Suspicious Source
|
||||
|
||||
#### Possible investigation steps
|
||||
|
||||
@@ -77,22 +77,48 @@ timestamp_override = "event.ingested"
|
||||
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
|
||||
| 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")
|
||||
from logs-o365.audit-*, logs-azure.signinlogs-*, .alerts-security.*
|
||||
// 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 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
|
||||
'''
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user