Loading

Multiple Microsoft 365 User Account Lockouts in Short Time Window

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.

Rule type: esql
Rule indices:

Rule Severity: medium
Risk Score: 47
Runs every:
Searches indices from: now-9m
Maximum alerts per execution: ?
References:

Tags:

  • Domain: Cloud
  • Domain: SaaS
  • Data Source: Microsoft 365
  • Data Source: Microsoft 365 Audit Logs
  • Use Case: Threat Detection
  • Use Case: Identity and Access Audit
  • Tactic: Credential Access
  • Resources: Investigation Guide

Version: ?
Rule authors:

  • Elastic

Rule license: Elastic License v2

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 ES|QL 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.

  • 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.
  • 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.
  • 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.
FROM logs-o365.audit-*

| MV_EXPAND event.category
| EVAL
    time_window = DATE_TRUNC(5 minutes, @timestamp),
    user_id = TO_LOWER(o365.audit.UserId),
    ip = source.ip,
    login_error = o365.audit.LogonError,
    request_type = TO_LOWER(o365.audit.ExtendedProperties.RequestType),
    asn_org = source.`as`.organization.name,
    country = source.geo.country_name,
    event_time = @timestamp

| WHERE event.dataset == "o365.audit"
  AND event.category == "authentication"
  AND event.provider IN ("AzureActiveDirectory", "Exchange")
  AND event.action IN ("UserLoginFailed", "PasswordLogonInitialAuthUsingPassword")
  AND request_type RLIKE "(oauth.*||.*login.*)"
  AND login_error == "IdsLocked"
  AND user_id != "not available"
  AND o365.audit.Target.Type IN ("0", "2", "6", "10")
  AND asn_org != "MICROSOFT-CORP-MSN-AS-BLOCK"

| STATS
    unique_users = COUNT_DISTINCT(user_id),
    user_id_list = VALUES(user_id),
    ip_list = VALUES(ip),
    unique_ips = COUNT_DISTINCT(ip),
    source_orgs = VALUES(asn_org),
    countries = VALUES(country),
    unique_country_count = COUNT_DISTINCT(country),
    unique_asn_orgs = COUNT_DISTINCT(asn_org),
    request_types = VALUES(request_type),
    first_seen = MIN(event_time),
    last_seen = MAX(event_time),
    total_lockout_responses = COUNT()
  BY time_window

| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen)

| KEEP
    time_window, unique_users, user_id_list, ip_list,
    unique_ips, source_orgs, countries, unique_country_count,
    unique_asn_orgs, request_types, first_seen, last_seen,
    total_lockout_responses, duration_seconds

| WHERE
    unique_users >= 10 AND
    total_lockout_responses >= 10 AND
    duration_seconds <= 300

Framework: MITRE ATT&CK