M365 Identity User Account Lockouts

Detects a burst of Microsoft 365 user account lockouts within a short 5-minute window. A high number of IdsLocked login errors across multiple user accounts may indicate brute-force attempts for the same users resulting in lockouts.

Elastic rule (View on GitHub)

  1[metadata]
  2creation_date = "2025/05/10"
  3integration = ["o365"]
  4maturity = "production"
  5updated_date = "2026/04/10"
  6
  7[rule]
  8author = ["Elastic"]
  9description = """
 10Detects a burst of Microsoft 365 user account lockouts within a short 5-minute window. A high number of IdsLocked login
 11errors across multiple user accounts may indicate brute-force attempts for the same users resulting in lockouts.
 12"""
 13from = "now-9m"
 14interval = "8m"
 15language = "esql"
 16license = "Elastic License v2"
 17name = "M365 Identity User Account Lockouts"
 18note = """## Triage and Analysis
 19
 20### Investigating M365 Identity User Account Lockouts
 21
 22Detects a burst of Microsoft 365 user account lockouts within a short 5-minute window. A high number of IdsLocked login errors across multiple user accounts may indicate brute-force attempts for the same users resulting in lockouts.
 23
 24This rule uses ESQL aggregations and thus has dynamically generated fields. Correlation of the values in the alert document may need to be performed to the original sign-in and Graph events for further context.
 25
 26### Investigation Steps
 27
 28- Review the `user_id_list`: Are specific naming patterns targeted (e.g., admin, helpdesk)?
 29- Examine `ip_list` and `source_orgs`: Look for suspicious ISPs or hosting providers.
 30- Check `duration_seconds`: A very short window with a high lockout rate often indicates automation.
 31- Confirm lockout policy thresholds with IAM or Entra ID admins. Did the policy trigger correctly?
 32- Use the `first_seen` and `last_seen` values to pivot into related authentication or audit logs.
 33- Correlate with any recent detection of password spraying or credential stuffing activity.
 34- Review the `request_type` field to identify which authentication methods were used (e.g., OAuth, SAML, etc.).
 35- Check for any successful logins from the same IP or ASN after the lockouts.
 36
 37### False Positive Analysis
 38
 39- Automated systems with stale credentials may cause repeated failed logins.
 40- Legitimate bulk provisioning or scripted tests could unintentionally cause account lockouts.
 41- Red team exercises or penetration tests may resemble the same lockout pattern.
 42- Some organizations may have a high volume of lockouts due to user behavior or legacy systems.
 43
 44### Response Recommendations
 45
 46- Notify affected users and confirm whether activity was expected or suspicious.
 47- Lock or reset credentials for impacted accounts.
 48- Block the source IP(s) or ASN temporarily using conditional access or firewall rules.
 49- Strengthen lockout and retry delay policies if necessary.
 50- Review the originating application(s) involved via `request_types`.
 51"""
 52references = [
 53    "https://learn.microsoft.com/en-us/security/operations/incident-response-playbook-password-spray",
 54    "https://learn.microsoft.com/en-us/purview/audit-log-detailed-properties",
 55    "https://securityscorecard.com/research/massive-botnet-targets-m365-with-stealthy-password-spraying-attacks/",
 56    "https://github.com/0xZDH/Omnispray",
 57    "https://github.com/0xZDH/o365spray",
 58]
 59risk_score = 47
 60rule_id = "de67f85e-2d43-11f0-b8c9-f661ea17fbcc"
 61severity = "medium"
 62tags = [
 63    "Domain: Cloud",
 64    "Domain: SaaS",
 65    "Data Source: Microsoft 365",
 66    "Data Source: Microsoft 365 Audit Logs",
 67    "Use Case: Threat Detection",
 68    "Use Case: Identity and Access Audit",
 69    "Tactic: Credential Access",
 70    "Resources: Investigation Guide",
 71
 72]
 73timestamp_override = "event.ingested"
 74type = "esql"
 75
 76query = '''
 77from logs-o365.audit-*
 78| mv_expand event.category
 79| eval
 80    Esql.time_window_date_trunc = date_trunc(5 minutes, @timestamp)
 81| where
 82    data_stream.dataset == "o365.audit" and
 83    event.category == "authentication" and
 84    event.provider in ("AzureActiveDirectory", "Exchange") and
 85    event.action in ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword") and
 86    to_lower(o365.audit.ExtendedProperties.RequestType) rlike "(oauth.*||.*login.*)" and
 87    o365.audit.LogonError == "IdsLocked" and
 88    to_lower(o365.audit.UserId) != "not available" and
 89    o365.audit.Target.Type in ("0", "2", "6", "10") and
 90    source.`as`.organization.name != "MICROSOFT-CORP-MSN-as-BLOCK"
 91| stats
 92    Esql_priv.o365_audit_UserId_count_distinct = count_distinct(to_lower(o365.audit.UserId)),
 93    Esql_priv.o365_audit_UserId_values = values(to_lower(o365.audit.UserId)),
 94    Esql.source_ip_values = values(source.ip),
 95    Esql.source_ip_count_distinct = count_distinct(source.ip),
 96    Esql.source_as_organization_name_values = values(source.`as`.organization.name),
 97    Esql.source_as_organization_name_count_distinct = count_distinct(source.`as`.organization.name),
 98    Esql.source_geo_country_name_values = values(source.geo.country_name),
 99    Esql.source_geo_country_name_count_distinct = count_distinct(source.geo.country_name),
100    Esql.o365_audit_ExtendedProperties_RequestType_values = values(to_lower(o365.audit.ExtendedProperties.RequestType)),
101    Esql.timestamp_first_seen = min(@timestamp),
102    Esql.timestamp_last_seen = max(@timestamp),
103    Esql.event_count = count(*)
104  by Esql.time_window_date_trunc
105| eval
106    Esql.event_duration_seconds = date_diff("seconds", Esql.timestamp_first_seen, Esql.timestamp_last_seen)
107| keep
108    Esql.time_window_date_trunc,
109    Esql_priv.o365_audit_UserId_count_distinct,
110    Esql_priv.o365_audit_UserId_values,
111    Esql.source_ip_values,
112    Esql.source_ip_count_distinct,
113    Esql.source_as_organization_name_values,
114    Esql.source_as_organization_name_count_distinct,
115    Esql.source_geo_country_name_values,
116    Esql.source_geo_country_name_count_distinct,
117    Esql.o365_audit_ExtendedProperties_RequestType_values,
118    Esql.timestamp_first_seen,
119    Esql.timestamp_last_seen,
120    Esql.event_count,
121    Esql.event_duration_seconds
122| where
123    Esql_priv.o365_audit_UserId_count_distinct >= 10 and
124    Esql.event_count >= 10 and
125    Esql.event_duration_seconds <= 300
126'''
127
128
129[[rule.threat]]
130framework = "MITRE ATT&CK"
131[[rule.threat.technique]]
132id = "T1110"
133name = "Brute Force"
134reference = "https://attack.mitre.org/techniques/T1110/"
135[[rule.threat.technique.subtechnique]]
136id = "T1110.001"
137name = "Password Guessing"
138reference = "https://attack.mitre.org/techniques/T1110/001/"
139
140[[rule.threat.technique.subtechnique]]
141id = "T1110.003"
142name = "Password Spraying"
143reference = "https://attack.mitre.org/techniques/T1110/003/"
144
145[[rule.threat.technique.subtechnique]]
146id = "T1110.004"
147name = "Credential Stuffing"
148reference = "https://attack.mitre.org/techniques/T1110/004/"
149
150
151
152[rule.threat.tactic]
153id = "TA0006"
154name = "Credential Access"
155reference = "https://attack.mitre.org/tactics/TA0006/"
156
157[[rule.threat]]
158framework = "MITRE ATT&CK"
159[[rule.threat.technique]]
160id = "T1078"
161name = "Valid Accounts"
162reference = "https://attack.mitre.org/techniques/T1078/"
163
164[[rule.threat.technique.subtechnique]]
165id = "T1078.004"
166name = "Cloud Accounts"
167reference = "https://attack.mitre.org/techniques/T1078/004/"
168
169[rule.threat.tactic]
170id = "TA0001"
171name = "Initial Access"
172reference = "https://attack.mitre.org/tactics/TA0001/"

Triage and Analysis

Investigating M365 Identity User Account Lockouts

Detects a burst of Microsoft 365 user account lockouts within a short 5-minute window. A high number of IdsLocked login errors across multiple user accounts may indicate brute-force attempts for the same users resulting in lockouts.

This rule uses ESQL aggregations and thus has dynamically generated fields. Correlation of the values in the alert document may need to be performed to the original sign-in and Graph events for further context.

Investigation Steps

  • Review the user_id_list: Are specific naming patterns targeted (e.g., admin, helpdesk)?
  • Examine ip_list and source_orgs: Look for suspicious ISPs or hosting providers.
  • Check duration_seconds: A very short window with a high lockout rate often indicates automation.
  • Confirm lockout policy thresholds with IAM or Entra ID admins. Did the policy trigger correctly?
  • Use the first_seen and last_seen values to pivot into related authentication or audit logs.
  • Correlate with any recent detection of password spraying or credential stuffing activity.
  • Review the request_type field to identify which authentication methods were used (e.g., OAuth, SAML, etc.).
  • Check for any successful logins from the same IP or ASN after the lockouts.

False Positive Analysis

  • Automated systems with stale credentials may cause repeated failed logins.
  • Legitimate bulk provisioning or scripted tests could unintentionally cause account lockouts.
  • Red team exercises or penetration tests may resemble the same lockout pattern.
  • Some organizations may have a high volume of lockouts due to user behavior or legacy systems.

Response Recommendations

  • Notify affected users and confirm whether activity was expected or suspicious.
  • Lock or reset credentials for impacted accounts.
  • Block the source IP(s) or ASN temporarily using conditional access or firewall rules.
  • Strengthen lockout and retry delay policies if necessary.
  • Review the originating application(s) involved via request_types.

References

Related rules

to-top