Skip to content
Open
Changes from all commits
Commits
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
Expand Up @@ -2,7 +2,8 @@
creation_date = "2025/07/01"
integration = ["azure"]
maturity = "production"
updated_date = "2025/09/26"
min_stack_version = "8.19.7"
updated_date = "2025/11/13"

[rule]
author = ["Elastic"]
Expand All @@ -18,10 +19,11 @@ false_positives = [
""",
]
from = "now-60m"
interval = "15m"
language = "esql"
index = ["filebeat-*", "logs-azure.signinlogs-*"]
interval = "30m"
language = "kuery"
license = "Elastic License v2"
name = "Microsoft Entra ID Exccessive Account Lockouts Detected"
name = "Microsoft Entra ID Excessive Account Lockouts Detected"
note = """## Triage and analysis

### Investigating Microsoft Entra ID Exccessive Account Lockouts Detected
Expand All @@ -30,14 +32,15 @@ This rule detects a high number of sign-in failures due to account lockouts (err

### Possible investigation steps

- Review `user_id_list` and `user_principal_name`: Check if targeted users include high-value accounts such as administrators, service principals, or shared inboxes.
- Check `error_codes` and `result_description`: Validate that `50053` (account locked) is the consistent failure type. Messages indicating "malicious IP" activity suggest Microsoft’s backend flagged the source.
- Analyze `ip_list` and `source_orgs`: Identify whether the activity originated from known malicious infrastructure (e.g., VPNs, botnets, or public cloud providers). In the example, traffic originates from `MASSCOM`, which should be validated.
- Inspect `device_detail_browser` and `user_agent`: Clients like `"Python Requests"` indicate scripted automation rather than legitimate login attempts.
- Evaluate `unique_users` vs. `total_attempts`: A high ratio suggests distributed attacks across multiple accounts, characteristic of password spraying.
- Correlate `client_app_display_name` and `incoming_token_type`: PowerShell or unattended sign-in clients may be targeted for automation or legacy auth bypass.
- Review `conditional_access_status` and `risk_state`: If Conditional Access was not applied and risk was not flagged, policy scope or coverage should be reviewed.
- Validate time range (`first_seen`, `last_seen`): Determine whether the attack is a short burst or part of a longer campaign.
Please note this is as threshold rule that aggregates multiple account lockouts over a specified time window. To properly investigate, pivot into the individual sign-in log events that contributed to the threshold being met.

- Review users impacted by pivoting searching for `user.name` in events where `azure.signinlogs.properties.status.error_code` is `50053`.
- Analyze source addresses associated with these lockouts. Identify whether the activity originated from known malicious infrastructure (e.g., VPNs, botnets, or public cloud providers).
- Inspect the user-agents involved in these account lockouts. Clients like `Python Requests` indicate scripted automation rather than legitimate login attempts. ROPC agents may suggest brute-forcing against legacy auth.
- A high ratio suggests distributed attacks across multiple accounts, characteristic of password spraying.
- Correlate client apps associated such as PowerShell or unattended sign-in clients may be targeted for automation or legacy auth bypass.
- Review conditional access state or risk state of the user involved. If Conditional Access was not applied and risk was not flagged, policy scope or coverage should be reviewed.
- Check for any successful sign-ins for the affected users around the same time frame to determine if any accounts were compromised prior to lockout.

### False positive analysis

Expand All @@ -55,6 +58,7 @@ This rule detects a high number of sign-in failures due to account lockouts (err
- Audit authentication methods in use, and enforce modern auth (OAuth, SAML) over legacy protocols.
- Strengthen Conditional Access policies to reduce exposure from weak locations, apps, or clients.
- Conduct credential hygiene audits to assess reuse and rotation for targeted accounts.
- If false positives are identified, create exceptions for known benign sources, users or user agents to reduce noise.
"""
references = [
"https://www.microsoft.com/en-us/security/blog/2025/05/27/new-russia-affiliated-actor-void-blizzard-targets-critical-sectors-for-espionage/",
Expand All @@ -81,104 +85,27 @@ tags = [
"Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"
type = "threshold"

query = '''
from logs-azure.signinlogs-*

| eval
Esql.time_window_date_trunc = date_trunc(30 minutes, @timestamp),
Esql_priv.azure_signinlogs_properties_user_principal_name_lower = to_lower(azure.signinlogs.properties.user_principal_name),
Esql.azure_signinlogs_properties_incoming_token_type_lower = to_lower(azure.signinlogs.properties.incoming_token_type),
Esql.azure_signinlogs_properties_app_display_name_lower = to_lower(azure.signinlogs.properties.app_display_name)

| where event.dataset == "azure.signinlogs"
and event.category == "authentication"
and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs")
and event.outcome == "failure"
and azure.signinlogs.properties.authentication_requirement == "singleFactorAuthentication"
and azure.signinlogs.properties.status.error_code == 50053
and azure.signinlogs.properties.user_principal_name is not null
and azure.signinlogs.properties.user_principal_name != ""
and source.`as`.organization.name != "MICROSOFT-CORP-MSN-as-BLOCK"

| stats
Esql.azure_signinlogs_properties_authentication_requirement_values = values(azure.signinlogs.properties.authentication_requirement),
Esql.azure_signinlogs_properties_app_id_values = values(azure.signinlogs.properties.app_id),
Esql.azure_signinlogs_properties_app_display_name_values = values(azure.signinlogs.properties.app_display_name),
Esql.azure_signinlogs_properties_resource_id_values = values(azure.signinlogs.properties.resource_id),
Esql.azure_signinlogs_properties_resource_display_name_values = values(azure.signinlogs.properties.resource_display_name),
Esql.azure_signinlogs_properties_conditional_access_status_values = values(azure.signinlogs.properties.conditional_access_status),
Esql.azure_signinlogs_properties_device_detail_browser_values = values(azure.signinlogs.properties.device_detail.browser),
Esql.azure_signinlogs_properties_device_detail_device_id_values = values(azure.signinlogs.properties.device_detail.device_id),
Esql.azure_signinlogs_properties_device_detail_operating_system_values = values(azure.signinlogs.properties.device_detail.operating_system),
Esql.azure_signinlogs_properties_incoming_token_type_values = values(azure.signinlogs.properties.incoming_token_type),
Esql.azure_signinlogs_properties_risk_state_values = values(azure.signinlogs.properties.risk_state),
Esql.azure_signinlogs_properties_session_id_values = values(azure.signinlogs.properties.session_id),
Esql.azure_signinlogs_properties_user_id_values = values(azure.signinlogs.properties.user_id),
Esql_priv.azure_signinlogs_properties_user_principal_name_values = values(azure.signinlogs.properties.user_principal_name),
Esql.azure_signinlogs_result_description_values = values(azure.signinlogs.result_description),
Esql.azure_signinlogs_result_signature_values = values(azure.signinlogs.result_signature),
Esql.azure_signinlogs_result_type_values = values(azure.signinlogs.result_type),

Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct = count_distinct(Esql_priv.azure_signinlogs_properties_user_principal_name_lower),
Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values = values(Esql_priv.azure_signinlogs_properties_user_principal_name_lower),
Esql.azure_signinlogs_result_description_count_distinct = count_distinct(azure.signinlogs.result_description),
Esql.azure_signinlogs_properties_status_error_code_count_distinct = count_distinct(azure.signinlogs.properties.status.error_code),
Esql.azure_signinlogs_properties_status_error_code_values = values(azure.signinlogs.properties.status.error_code),
Esql.azure_signinlogs_properties_incoming_token_type_lower_values = values(Esql.azure_signinlogs_properties_incoming_token_type_lower),
Esql.azure_signinlogs_properties_app_display_name_lower_values = values(Esql.azure_signinlogs_properties_app_display_name_lower),
Esql.source_ip_values = values(source.ip),
Esql.source_ip_count_distinct = count_distinct(source.ip),
Esql.source_as_organization_name_values = values(source.`as`.organization.name),
Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name),
Esql.source_geo_country_name_values = values(source.geo.country_name),
Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name),
Esql.@timestamp.min = min(@timestamp),
Esql.@timestamp.max = max(@timestamp),
Esql.event_count = count()
by Esql.time_window_date_trunc

| where Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct >= 15 and Esql.event_count >= 20

| keep
Esql.time_window_date_trunc,
Esql.event_count,
Esql.@timestamp.min,
Esql.@timestamp.max,
Esql.azure_signinlogs_properties_user_principal_name_lower_count_distinct,
Esql_priv.azure_signinlogs_properties_user_principal_name_lower_values,
Esql.azure_signinlogs_result_description_count_distinct,
Esql.azure_signinlogs_result_description_values,
Esql.azure_signinlogs_properties_status_error_code_count_distinct,
Esql.azure_signinlogs_properties_status_error_code_values,
Esql.azure_signinlogs_properties_incoming_token_type_lower_values,
Esql.azure_signinlogs_properties_app_display_name_lower_values,
Esql.source_ip_values,
Esql.source_ip_count_distinct,
Esql.source_as_organization_name_values,
Esql.source_as_organization_name_count_distinct,
Esql.source_geo_country_name_values,
Esql.source_geo_country_name_count_distinct,
Esql.azure_signinlogs_properties_authentication_requirement_values,
Esql.azure_signinlogs_properties_app_id_values,
Esql.azure_signinlogs_properties_app_display_name_values,
Esql.azure_signinlogs_properties_resource_id_values,
Esql.azure_signinlogs_properties_resource_display_name_values,
Esql.azure_signinlogs_properties_conditional_access_status_values,
Esql.azure_signinlogs_properties_device_detail_browser_values,
Esql.azure_signinlogs_properties_device_detail_device_id_values,
Esql.azure_signinlogs_properties_device_detail_operating_system_values,
Esql.azure_signinlogs_properties_incoming_token_type_values,
Esql.azure_signinlogs_properties_risk_state_values,
Esql.azure_signinlogs_properties_session_id_values,
Esql.azure_signinlogs_properties_user_id_values,
Esql_priv.azure_signinlogs_properties_user_principal_name_values,
Esql.azure_signinlogs_result_description_values,
Esql.azure_signinlogs_result_signature_values,
Esql.azure_signinlogs_result_type_values
event.dataset: "azure.signinlogs" and event.category: "authentication"
and azure.signinlogs.category: ("NonInteractiveUserSignInLogs" or "SignInLogs")
and event.outcome: "failure"
and azure.signinlogs.properties.authentication_requirement: "singleFactorAuthentication"
and azure.signinlogs.properties.status.error_code: 50053
and azure.signinlogs.properties.user_principal_name: *
and not azure.signinlogs.properties.user_principal_name: ""
and not source.as.organization.name: "MICROSOFT-CORP-MSN-as-BLOCK"
'''

[rule.threshold]
field = []
value = 20

[[rule.threshold.cardinality]]
field = "user.name"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@terrancedejesus have u tried to simulate this rule and see the if from the corresponding alert analyst can exclude specific user.name as FP (similar to what the original SDH objective) ?

value = 15


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