LoFP LoFP / automated processes that attempt to authenticate using expired credentials or have misconfigured authentication settings may lead to false positives.

Sample rules

Microsoft Entra ID Exccessive Account Lockouts Detected

Description

Identifies a high count of failed Microsoft Entra ID sign-in attempts as the result of the target user account being locked out. Adversaries may attempt to brute-force user accounts by repeatedly trying to authenticate with incorrect credentials, leading to account lockouts by Entra ID Smart Lockout policies.

Detection logic

FROM logs-azure.signinlogs*

| EVAL
    time_window = DATE_TRUNC(30 minutes, @timestamp),
    user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name),
    ip = source.ip,
    login_error = azure.signinlogs.result_description,
    error_code = azure.signinlogs.properties.status.error_code,
    request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type),
    app_name = TO_LOWER(azure.signinlogs.properties.app_display_name),
    asn_org = source.`as`.organization.name,
    country = source.geo.country_name,
    user_agent = user_agent.original,
    event_time = @timestamp

| 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 error_code == 50053
    AND user_id IS NOT NULL AND user_id != ""
    AND asn_org != "MICROSOFT-CORP-MSN-AS-BLOCK"

| STATS
    authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
    client_app_id = VALUES(azure.signinlogs.properties.app_id),
    client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
    target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
    target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
    conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
    device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
    device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
    device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system),
    incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
    risk_state = VALUES(azure.signinlogs.properties.risk_state),
    session_id = VALUES(azure.signinlogs.properties.session_id),
    user_id = VALUES(azure.signinlogs.properties.user_id),
    user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
    result_description = VALUES(azure.signinlogs.result_description),
    result_signature = VALUES(azure.signinlogs.result_signature),
    result_type = VALUES(azure.signinlogs.result_type),

    unique_users = COUNT_DISTINCT(user_id),
    user_id_list = VALUES(user_id),
    login_errors = VALUES(login_error),
    unique_login_errors = COUNT_DISTINCT(login_error),
    error_codes = VALUES(error_code),
    unique_error_codes = COUNT_DISTINCT(error_code),
    request_types = VALUES(request_type),
    app_names = VALUES(app_name),
    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),
    first_seen = MIN(event_time),
    last_seen = MAX(event_time),
    total_attempts = COUNT()
BY time_window
| WHERE unique_users >= 15 AND total_attempts >= 20
| KEEP
    time_window, total_attempts, first_seen, last_seen,
    unique_users, user_id_list, login_errors, unique_login_errors,
    unique_error_codes, error_codes, request_types, app_names,
    ip_list, unique_ips, source_orgs, countries,
    unique_country_count, unique_asn_orgs,
    authentication_requirement, client_app_id, client_app_display_name,
    target_resource_id, target_resource_display_name, conditional_access_status,
    device_detail_browser, device_detail_device_id, device_detail_operating_system,
    incoming_token_type, risk_state, session_id, user_id,
    user_principal_name, result_description, result_signature, result_type

Microsoft Entra ID Sign-In Brute Force Activity

Description

Identifies potential brute-force attacks targeting user accounts by analyzing failed sign-in patterns in Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or password guessing. Adversaries may use these techniques to gain unauthorized access to applications integrated with Entra ID or to compromise valid user accounts.

Detection logic

FROM logs-azure.signinlogs*

// Define a time window for grouping and maintain the original event timestamp
| EVAL
    time_window = DATE_TRUNC(15 minutes, @timestamp),
    event_time = @timestamp

// Filter relevant failed authentication events with specific error codes
| 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 IN (
        50034,  // UserAccountNotFound
        50126,  // InvalidUsernameOrPassword
        50055,  // PasswordExpired
        50056,  // InvalidPassword
        50057,  // UserDisabled
        50064,  // CredentialValidationFailure
        50076,  // MFARequiredButNotPassed
        50079,  // MFARegistrationRequired
        50105,  // EntitlementGrantsNotFound
        70000,  // InvalidGrant
        70008,  // ExpiredOrRevokedRefreshToken
        70043,  // BadTokenDueToSignInFrequency
        80002,  // OnPremisePasswordValidatorRequestTimedOut
        80005,  // OnPremisePasswordValidatorUnpredictableWebException
        50144,  // InvalidPasswordExpiredOnPremPassword
        50135,  // PasswordChangeCompromisedPassword
        50142,  // PasswordChangeRequiredConditionalAccess
        120000, // PasswordChangeIncorrectCurrentPassword
        120002, // PasswordChangeInvalidNewPasswordWeak
        120020  // PasswordChangeFailure
    )
    AND azure.signinlogs.properties.user_principal_name IS NOT NULL AND azure.signinlogs.properties.user_principal_name != ""
    AND user_agent.original != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"
    AND source.`as`.organization.name != "MICROSOFT-CORP-MSN-AS-BLOCK"

// Aggregate statistics for behavioral pattern analysis
| STATS
    authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
    client_app_id = VALUES(azure.signinlogs.properties.app_id),
    client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
    target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
    target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
    conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
    device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
    device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
    device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system),
    incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
    risk_state = VALUES(azure.signinlogs.properties.risk_state),
    session_id = VALUES(azure.signinlogs.properties.session_id),
    user_id = VALUES(azure.signinlogs.properties.user_id),
    user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
    result_description = VALUES(azure.signinlogs.result_description),
    result_signature = VALUES(azure.signinlogs.result_signature),
    result_type = VALUES(azure.signinlogs.result_type),

    unique_users = COUNT_DISTINCT(azure.signinlogs.properties.user_id),
    user_id_list = VALUES(azure.signinlogs.properties.user_id),
    login_errors = VALUES(azure.signinlogs.result_description),
    unique_login_errors = COUNT_DISTINCT(azure.signinlogs.result_description),
    error_codes = VALUES(azure.signinlogs.properties.status.error_code),
    unique_error_codes = COUNT_DISTINCT(azure.signinlogs.properties.status.error_code),
    request_types = VALUES(azure.signinlogs.properties.incoming_token_type),
    app_names = VALUES(azure.signinlogs.properties.app_display_name),
    ip_list = VALUES(source.ip),
    unique_ips = COUNT_DISTINCT(source.ip),
    source_orgs = VALUES(source.`as`.organization.name),
    countries = VALUES(source.geo.country_name),
    unique_country_count = COUNT_DISTINCT(source.geo.country_name),
    unique_asn_orgs = COUNT_DISTINCT(source.`as`.organization.name),
    first_seen = MIN(@timestamp),
    last_seen = MAX(@timestamp),
    total_attempts = COUNT()
BY time_window

// Determine brute force behavior type based on statistical thresholds
| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    bf_type = CASE(
        // Many users, relatively few distinct login errors, distributed over multiple IPs (but not too many),
        // and happens quickly. Often bots using leaked credentials.
        unique_users >= 10 AND total_attempts >= 30 AND unique_login_errors <= 3
            AND unique_ips >= 5
            AND duration_seconds <= 600
            AND unique_users > unique_ips,
        "credential_stuffing",

        // One password against many users. Single error (e.g., "InvalidPassword"), not necessarily fast.
        unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 15 AND duration_seconds <= 1800,
        "password_spraying",

        // One user targeted repeatedly (same error), OR extremely noisy pattern from many IPs.
        (unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300)
            OR (unique_users <= 3 AND unique_ips > 30 AND total_attempts >= 100),
        "password_guessing",

        // everything else
        "other"
    )

// Only keep columns necessary for detection output/reporting
| KEEP
    time_window, bf_type, duration_seconds, total_attempts, first_seen, last_seen,
    unique_users, user_id_list, login_errors, unique_login_errors,
    unique_error_codes, error_codes, request_types, app_names,
    ip_list, unique_ips, source_orgs, countries,
    unique_country_count, unique_asn_orgs,
    authentication_requirement, client_app_id, client_app_display_name,
    target_resource_id, target_resource_display_name, conditional_access_status,
    device_detail_browser, device_detail_device_id, device_detail_operating_system,
    incoming_token_type, risk_state, session_id, user_id,
    user_principal_name, result_description, result_signature, result_type

// Remove anything not classified as credential attack activity
| WHERE bf_type != "other"

Microsoft 365 Brute Force via Entra ID Sign-Ins

Description

Identifies potential brute-force attacks targeting Microsoft 365 user accounts by analyzing failed sign-in patterns in Microsoft Entra ID Sign-In Logs. This detection focuses on a high volume of failed interactive or non-interactive authentication attempts within a short time window, often indicative of password spraying, credential stuffing, or password guessing. Adversaries may use these techniques to gain unauthorized access to Microsoft 365 services such as Exchange Online, SharePoint, or Teams.

Detection logic

FROM logs-azure.signinlogs*

| EVAL
    time_window = DATE_TRUNC(15 minutes, @timestamp),
    user_id = TO_LOWER(azure.signinlogs.properties.user_principal_name),
    ip = source.ip,
    login_error = azure.signinlogs.result_description,
    error_code = azure.signinlogs.properties.status.error_code,
    request_type = TO_LOWER(azure.signinlogs.properties.incoming_token_type),
    app_name = TO_LOWER(azure.signinlogs.properties.app_display_name),
    asn_org = source.`as`.organization.name,
    country = source.geo.country_name,
    user_agent = user_agent.original,
    event_time = @timestamp

| WHERE event.dataset == "azure.signinlogs"
    AND event.category == "authentication"
    AND azure.signinlogs.category IN ("NonInteractiveUserSignInLogs", "SignInLogs")
    AND azure.signinlogs.properties.resource_display_name RLIKE "(.*)365|SharePoint|Exchange|Teams|Office(.*)"
    AND event.outcome == "failure"
    AND error_code != 50053
    AND azure.signinlogs.properties.status.error_code IN (
        50034,  // UserAccountNotFound
        50126,  // InvalidUsernameOrPassword
        50055,  // PasswordExpired
        50056,  // InvalidPassword
        50057,  // UserDisabled
        50064,  // CredentialValidationFailure
        50076,  // MFARequiredButNotPassed
        50079,  // MFARegistrationRequired
        50105,  // EntitlementGrantsNotFound
        70000,  // InvalidGrant
        70008,  // ExpiredOrRevokedRefreshToken
        70043,  // BadTokenDueToSignInFrequency
        80002,  // OnPremisePasswordValidatorRequestTimedOut
        80005,  // OnPremisePasswordValidatorUnpredictableWebException
        50144,  // InvalidPasswordExpiredOnPremPassword
        50135,  // PasswordChangeCompromisedPassword
        50142,  // PasswordChangeRequiredConditionalAccess
        120000, // PasswordChangeIncorrectCurrentPassword
        120002, // PasswordChangeInvalidNewPasswordWeak
        120020  // PasswordChangeFailure
    )
    AND user_id IS NOT NULL AND user_id != ""
    AND user_agent != "Mozilla/5.0 (compatible; MSAL 1.0) PKeyAuth/1.0"

| STATS
    authentication_requirement = VALUES(azure.signinlogs.properties.authentication_requirement),
    client_app_id = VALUES(azure.signinlogs.properties.app_id),
    client_app_display_name = VALUES(azure.signinlogs.properties.app_display_name),
    target_resource_id = VALUES(azure.signinlogs.properties.resource_id),
    target_resource_display_name = VALUES(azure.signinlogs.properties.resource_display_name),
    conditional_access_status = VALUES(azure.signinlogs.properties.conditional_access_status),
    device_detail_browser = VALUES(azure.signinlogs.properties.device_detail.browser),
    device_detail_device_id = VALUES(azure.signinlogs.properties.device_detail.device_id),
    device_detail_operating_system = VALUES(azure.signinlogs.properties.device_detail.operating_system),
    incoming_token_type = VALUES(azure.signinlogs.properties.incoming_token_type),
    risk_state = VALUES(azure.signinlogs.properties.risk_state),
    session_id = VALUES(azure.signinlogs.properties.session_id),
    user_id = VALUES(azure.signinlogs.properties.user_id),
    user_principal_name = VALUES(azure.signinlogs.properties.user_principal_name),
    result_description = VALUES(azure.signinlogs.result_description),
    result_signature = VALUES(azure.signinlogs.result_signature),
    result_type = VALUES(azure.signinlogs.result_type),

    unique_users = COUNT_DISTINCT(user_id),
    user_id_list = VALUES(user_id),
    login_errors = VALUES(login_error),
    unique_login_errors = COUNT_DISTINCT(login_error),
    error_codes = VALUES(error_code),
    unique_error_codes = COUNT_DISTINCT(error_code),
    request_types = VALUES(request_type),
    app_names = VALUES(app_name),
    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),
    first_seen = MIN(event_time),
    last_seen = MAX(event_time),
    total_attempts = COUNT()
BY time_window

| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    bf_type = CASE(
        // Many users, relatively few distinct login errors, distributed over multiple IPs (but not too many),
        // and happens quickly. Often bots using leaked credentials.
        unique_users >= 10 AND total_attempts >= 30 AND unique_login_errors <= 3
            AND unique_ips >= 5
            AND duration_seconds <= 600
            AND unique_users > unique_ips,
        "credential_stuffing",

        // One password against many users. Single error (e.g., "InvalidPassword"), not necessarily fast.
        unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 15 AND duration_seconds <= 1800,
        "password_spraying",

        // One user targeted repeatedly (same error), OR extremely noisy pattern from many IPs.
        (unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 30 AND duration_seconds <= 300)
            OR (unique_users <= 3 AND unique_ips > 30 AND total_attempts >= 100),
        "password_guessing",

        // everything else
        "other"
    )

| KEEP
    time_window, bf_type, duration_seconds, total_attempts, first_seen, last_seen,
    unique_users, user_id_list, login_errors, unique_login_errors,
    unique_error_codes, error_codes, request_types, app_names,
    ip_list, unique_ips, source_orgs, countries,
    unique_country_count, unique_asn_orgs,
    authentication_requirement, client_app_id, client_app_display_name,
    target_resource_id, target_resource_display_name, conditional_access_status,
    device_detail_browser, device_detail_device_id, device_detail_operating_system,
    incoming_token_type, risk_state, session_id, user_id,
    user_principal_name, result_description, result_signature, result_type

| WHERE bf_type != "other"

Deprecated - Azure Entra Sign-in Brute Force Microsoft 365 Accounts by Repeat Source

Description

Identifies potential brute-force attempts against Microsoft 365 user accounts by detecting a high number of failed interactive or non-interactive login attempts within a 30-minute window from a single source. Attackers may attempt to brute force user accounts to gain unauthorized access to Microsoft 365 services via different services such as Exchange, SharePoint, or Teams.

Detection logic

from logs-azure.signinlogs*
| WHERE
  event.dataset == "azure.signinlogs"
  and event.category == "authentication"
  and to_lower(azure.signinlogs.properties.resource_display_name) rlike "(.*)365(.*)"
  and azure.signinlogs.category in ("NonInteractiveUserSignInLogs", "SignInLogs")
  and event.outcome != "success"

  // For tuning, review azure.signinlogs.properties.status.error_code
  // https://learn.microsoft.com/en-us/entra/identity-platform/reference-error-codes

// keep only relevant fields
| keep event.dataset, event.category, azure.signinlogs.properties.resource_display_name, azure.signinlogs.category, event.outcome, azure.signinlogs.properties.user_principal_name, source.ip

// Count the number of unique targets per source IP
| stats
  target_count = count_distinct(azure.signinlogs.properties.user_principal_name) by source.ip

// Filter for at least 10 distinct failed login attempts from a single source
| where target_count >= 10

Okta Brute Force or Password Spraying Attack

Description

Identifies a high number of failed Okta user authentication attempts from a single IP address, which could be indicative of a brute force or password spraying attack. An adversary may attempt a brute force or password spraying attack to obtain unauthorized access to user accounts.

Detection logic

event.dataset:okta.system and event.category:authentication and event.outcome:failure

AWS Management Console Brute Force of Root User Identity

Description

Identifies a high number of failed authentication attempts to the AWS management console for the Root user identity. An adversary may attempt to brute force the password for the Root user identity, as it has complete access to all services and resources for the AWS account.

Detection logic

event.dataset:aws.cloudtrail and event.provider:signin.amazonaws.com and event.action:ConsoleLogin and aws.cloudtrail.user_identity.type:Root and event.outcome:failure

O365 Excessive Single Sign-On Logon Errors

Description

Identifies accounts with a high number of single sign-on (SSO) logon errors. Excessive logon errors may indicate an attempt to brute force a password or SSO token.

Detection logic

event.dataset:o365.audit and event.provider:AzureActiveDirectory and event.category:authentication and o365.audit.LogonError:"SsoArtifactInvalidOrExpired"

Potential Microsoft 365 User Account Brute Force

Description

Identifies brute-force authentication activity targeting Microsoft 365 user accounts using failed sign-in patterns that match password spraying, credential stuffing, or password guessing behavior. Adversaries may attempt brute-force authentication with credentials obtained from previous breaches, leaks, marketplaces or guessable passwords.

Detection logic

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 login_error NOT IN (
    "EntitlementGrantsNotFound", "UserStrongAuthEnrollmentRequired", "UserStrongAuthClientAuthNRequired",
    "InvalidReplyTo", "SsoArtifactExpiredDueToConditionalAccess", "PasswordResetRegistrationRequiredInterrupt",
    "SsoUserAccountNotFoundInResourceTenant", "UserStrongAuthExpired", "CmsiInterrupt"
  )
  AND user_id != "not available"
  AND o365.audit.Target.Type IN ("0", "2", "6", "10")

| STATS
    unique_users = COUNT_DISTINCT(user_id),
    user_id_list = VALUES(user_id),
    login_errors = VALUES(login_error),
    unique_login_errors = COUNT_DISTINCT(login_error),
    request_types = VALUES(request_type),
    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),
    first_seen = MIN(event_time),
    last_seen = MAX(event_time),
    total_attempts = COUNT()
  BY time_window

| EVAL
    duration_seconds = DATE_DIFF("seconds", first_seen, last_seen),
    bf_type = CASE(
        unique_users >= 15 AND unique_login_errors == 1 AND total_attempts >= 10 AND duration_seconds <= 1800, "password_spraying",
        unique_users >= 8 AND total_attempts >= 15 AND unique_login_errors <= 3 AND unique_ips <= 5 AND duration_seconds <= 600, "credential_stuffing",
        unique_users == 1 AND unique_login_errors == 1 AND total_attempts >= 20 AND duration_seconds <= 300, "password_guessing",
        "other"
    )

| KEEP
    time_window, unique_users, user_id_list, login_errors, unique_login_errors,
    request_types, ip_list, unique_ips, source_orgs, countries,
    unique_country_count, unique_asn_orgs, first_seen, last_seen,
    duration_seconds, total_attempts, bf_type

| WHERE
    bf_type != "other"

Deprecated - Potential Password Spraying of Microsoft 365 User Accounts

Description

Identifies a high number (25) of failed Microsoft 365 user authentication attempts from a single IP address within 30 minutes, which could be indicative of a password spraying attack. An adversary may attempt a password spraying attack to obtain unauthorized access to user accounts.

Detection logic

event.dataset:o365.audit and event.provider:(Exchange or AzureActiveDirectory) and event.category:authentication and
event.action:("UserLoginFailed" or "PasswordLogonInitialAuthUsingPassword")