Query Details

02 SIGNIN Brute Force Success Chain

Query

id: d2e3f4a5-b6c7-4d8e-9f0a-1b2c3d4e5f6a
name: "SigninLogs — Brute Force Success Chain (Possible Account Breach)"
version: 1.0.0
kind: Scheduled
description: |
  Detects a successful sign-in occurring within 30 minutes of 5 or more failed attempts from the same IP against the same user. This pattern is a strong indicator of a successful breach following a brute force attack. MITRE ATT&CK: T1110 (Brute Force), T1078 (Valid Accounts).
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
queryFrequency: PT1H
queryPeriod: PT1H
triggerOperator: gt
triggerThreshold: 0
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1110
  - T1078
query: |

    // ---- Network Allowlist (exclude trusted IPs / CIDR / ranges) --------------
    let _allow = materialize(union isfuzzy=true (print R="" | take 0), (_GetWatchlist('NetworkAllowlist') | project R = tostring(IPOrRange)) | where isnotempty(R));
    let _allowCIDR  = toscalar(_allow | where not(R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$') | extend R = iff(R has '/', R, strcat(R, '/32')) | summarize make_list(R));
    let _allowRange = toscalar(_allow | where R matches regex @'^\d+\.\d+\.\d+\.\d+-\d+\.\d+\.\d+\.\d+$' | summarize make_list(R));
    let _ExcludeAllowlistedIPs = (T:(IPAddress:string)) {
        T
        | extend IPAddress = tostring(IPAddress)
        | where array_length(_allowCIDR) == 0 or isnull(ipv4_is_in_any_range(IPAddress, _allowCIDR)) or not(ipv4_is_in_any_range(IPAddress, _allowCIDR))
        | mv-apply _r = _allowRange to typeof(string) on (
            extend _lo = tostring(split(_r,'-')[0]), _hi = tostring(split(_r,'-')[1])
            | extend _inRange = ipv4_compare(IPAddress, _lo) >= 0 and ipv4_compare(IPAddress, _hi) <= 0
            | summarize _anyInRange = max(toint(_inRange)))
        | where isnull(_anyInRange) or _anyInRange == 0
        | project-away _anyInRange
    };
    // ---------------------------------------------------------------------------
    let timeframe        = 1h;
    let failureThreshold = 5;
    let successWindow    = 30m;
    let FailedLogins =
        SigninLogs
        | invoke _ExcludeAllowlistedIPs()
        | where TimeGenerated > ago(timeframe)
        | where ResultType != "0"
        | summarize
            FailureCount   = count(),
            LastFailure    = max(TimeGenerated),
            FailureReasons = make_set(ResultDescription, 5)
          by UserPrincipalName, IPAddress
        | where FailureCount >= failureThreshold;
    let SuccessfulLogins =
        SigninLogs
        | invoke _ExcludeAllowlistedIPs()
        | where TimeGenerated > ago(timeframe)
        | where ResultType == "0"
        | project
            UserPrincipalName, IPAddress,
            SuccessTime = TimeGenerated,
            Location, AppDisplayName,
            RiskLevelDuringSignIn;
    FailedLogins
    | join kind=inner SuccessfulLogins on UserPrincipalName, IPAddress
    | where SuccessTime > LastFailure
    | where SuccessTime - LastFailure <= successWindow
    | extend
        TimeSinceLastFailure = datetime_diff("minute", SuccessTime, LastFailure)
    | project
        TimeGenerated        = SuccessTime,
        UserPrincipalName, IPAddress, Location,
        AppDisplayName, FailureCount,
        TimeSinceLastFailure, FailureReasons,
        RiskLevelDuringSignIn
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
customDetails:
  FailureCount: FailureCount
  TimeSinceLastFailure: TimeSinceLastFailure
alertDetailsOverride:
  alertDisplayNameFormat: "Brute Force Breach — {{UserPrincipalName}} ({{FailureCount}} failures then success)"
  alertDescriptionFormat: "Sign-in from {{IPAddress}} succeeded after {{FailureCount}} failures — possible account compromise."
incidentConfiguration:
  createIncident: true
  groupingConfiguration:
    enabled: true
    reopenClosedIncident: false
    lookbackDuration: PT1H
    matchingMethod: AllEntities
    groupByEntities:
  - Account
  - IP



Explanation

This query is designed to detect potential account breaches through brute force attacks by analyzing sign-in logs. Here's a simplified explanation:

  1. Purpose: The query identifies successful sign-ins that occur within 30 minutes after five or more failed attempts from the same IP address targeting the same user. This pattern suggests a possible account breach following a brute force attack.

  2. Data Source: It uses sign-in logs from Azure Active Directory.

  3. Exclusions: The query excludes trusted IP addresses or ranges (allowlist) to reduce false positives.

  4. Process:

    • Failed Logins: It first identifies users with five or more failed login attempts from the same IP within the last hour.
    • Successful Logins: It then checks for successful logins from the same IP and user within 30 minutes after the last failed attempt.
    • Join and Filter: The query joins the failed and successful login data to find matches and calculates the time between the last failed attempt and the successful login.
  5. Output: The results include details like the user's principal name, IP address, location, application name, number of failed attempts, time since the last failure, reasons for failure, and risk level during sign-in.

  6. Alerting: If such a pattern is detected, an alert is generated with a high severity level, indicating a possible account compromise. The alert includes details like the number of failures before success and the IP address involved.

  7. Incident Management: The query is configured to create an incident for each detected case, with options for grouping similar incidents based on account and IP.

Overall, this query helps in identifying and alerting on potential security breaches due to brute force attacks, allowing for timely investigation and response.

Details

David Alonso profile picture

David Alonso

Released: April 20, 2026

Tables

SigninLogs

Keywords

SigninLogsAzureActiveDirectoryUserIPAddressLocationAppDisplayNameRiskLevelDuringSignInAccount

Operators

letmaterializeunionprinttakeprojectwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listnotisnullipv4_is_in_any_rangemv-applysplitipv4_comparemaxtointproject-awayagocountmake_setbyinvokejoinkind=innerdatetime_diff

Actions