← Back to SOC feed Coverage →

Short-window IP failure burst followed by successful sign-in

kql MEDIUM Azure-Sentinel
T1110.003T1078
AADNonInteractiveUserSignInLogsSigninLogs
credential-thefthuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-06-03T11:00:00Z · Confidence: medium

Hunt Hypothesis

Adversaries may attempt password spraying by failing sign-ins with multiple identities from a single IP address before successfully authenticating, indicating a coordinated credential compromise effort. SOC teams should proactively hunt for this pattern in Azure Sentinel to detect and mitigate potential credential reuse or password spraying attacks early.

KQL Query

let starttime = todatetime('{{StartTimeISO}}');
let endtime = todatetime('{{EndTimeISO}}');
let correlationWindow = 15m;
let minFailedUsersFromIP = 5;
let minFailedAttemptsFromIP = 20;
let maxSuccessfulUsersFromIP = 3;
let successCodes = dynamic(["0", "50125", "50140", "70043", "70044"]);
let signins =
    union isfuzzy=true
    (
        SigninLogs
        | where TimeGenerated between (starttime .. endtime)
        | project
            TimeGenerated,
            SourceTable = "SigninLogs",
            UserPrincipalName = tolower(UserPrincipalName),
            IPAddress,
            ResultType = tostring(ResultType),
            ResultDescription,
            AppDisplayName,
            ClientAppUsed,
            CorrelationId
    ),
    (
        AADNonInteractiveUserSignInLogs
        | where TimeGenerated between (starttime .. endtime)
        | project
            TimeGenerated,
            SourceTable = "AADNonInteractiveUserSignInLogs",
            UserPrincipalName = tolower(UserPrincipalName),
            IPAddress,
            ResultType = tostring(ResultType),
            ResultDescription,
            AppDisplayName,
            ClientAppUsed,
            CorrelationId
    )
    | where isnotempty(UserPrincipalName) and isnotempty(IPAddress);
let failedBursts =
    signins
    | where ResultType !in (successCodes)
    | summarize
        FirstFailure = min(TimeGenerated),
        LastFailure = max(TimeGenerated),
        FailedAttempts = count(),
        FailedUsers = dcount(UserPrincipalName),
        FailedUserSet = make_set(UserPrincipalName, 50),
        FailedAppSet = make_set(AppDisplayName, 20),
        FailedSourceSet = make_set(SourceTable, 2),
        FailureResultSet = make_set(ResultType, 20)
        by IPAddress, FailureWindowStart = bin(TimeGenerated, correlationWindow)
    | where FailedUsers >= minFailedUsersFromIP and FailedAttempts >= minFailedAttemptsFromIP;
let successes =
    signins
    | where ResultType in (successCodes)
    | project
        SuccessTime = TimeGenerated,
        IPAddress,
        SuccessUserPrincipalName = UserPrincipalName,
        SuccessSourceTable = SourceTable,
        SuccessAppDisplayName = AppDisplayName,
        SuccessClientApp = ClientAppUsed,
        SuccessCorrelationId = CorrelationId;
failedBursts
| join kind=inner successes on IPAddress
| where SuccessTime between (FirstFailure .. (LastFailure + correlationWindow))
| summarize
    FirstSuccess = min(SuccessTime),
    LastSuccess = max(SuccessTime),
    SuccessEvents = count(),
    SuccessfulUsersAfterFailures = dcount(SuccessUserPrincipalName),
    SuccessfulUserSet = make_set(SuccessUserPrincipalName, 25),
    SuccessSourceSet = make_set(SuccessSourceTable, 2),
    SuccessAppSet = make_set(SuccessAppDisplayName, 20),
    SuccessClientAppSet = make_set(SuccessClientApp, 20)
    by IPAddress,
    FailureWindowStart,
    FirstFailure,
    LastFailure,
    FailedAttempts,
    FailedUsers,
    FailedUserSet,
    FailedAppSet,
    FailedSourceSet,
    FailureResultSet
| where SuccessfulUsersAfterFailures > 0 and SuccessfulUsersAfterFailures <= maxSuccessfulUsersFromIP
| extend ExposureRatio = round(todouble(FailedUsers) / todouble(SuccessfulUsersAfterFailures), 2)
| extend FirstSuccessfulUser = tostring(SuccessfulUserSet[0])
| extend AccountName = tostring(split(FirstSuccessfulUser, "@")[0]),
         AccountUPNSuffix = tostring(split(FirstSuccessfulUser, "@")[1])
| project
    FirstFailure,
    LastFailure,
    FirstSuccess,
    LastSuccess,
    IPAddress,
    FailedAttempts,
    FailedUsers,
    SuccessfulUsersAfterFailures,
    ExposureRatio,
    FailedUserSet,
    SuccessfulUserSet,
    FailedAppSet,
    SuccessAppSet,
    FailedSourceSet,
    SuccessSourceSet,
    FailureResultSet,
    SuccessEvents,
    FirstSuccessfulUser,
    AccountName,
    AccountUPNSuffix
| order by FailedUsers desc, FailedAttempts desc, FirstFailure desc
| extend timestamp = FirstFailure, IPCustomEntity = IPAddress, AccountCustomEntity = FirstSuccessfulUser

Analytic Rule Definition

id: 5c3a480b-d7a8-4a9c-a6b5-5bb2e3ebac89
name: Short-window IP failure burst followed by successful sign-in
description: |
  Hunt for IP addresses that fail sign-ins against multiple identities and then
  authenticate successfully within a short time window. This can highlight
  password spraying or credential misuse patterns and requires analyst validation.
description-detailed: |
  This hypothesis-driven Microsoft Sentinel hunting query correlates interactive
  (SigninLogs) and non-interactive (AADNonInteractiveUserSignInLogs) Entra ID
  sign-ins by source IP address. It identifies IPs that generate a burst of failed
  sign-ins across multiple user identities and then record one or more successful
  sign-ins within a short correlation window.
  This pattern can be consistent with password spraying or opportunistic credential
  misuse, but it is not proof of malicious activity on its own. Analysts should
  validate each result with identity context, device context, and expected network
  behavior. Benign causes can include shared egress points, NAT, VPN gateways,
  enterprise proxies, scripted health checks, and timing overlap from legitimate
  background application activity.
  References:
  - https://learn.microsoft.com/azure/active-directory/reports-monitoring/concept-sign-ins
  - https://attack.mitre.org/techniques/T1110/003/
  - https://attack.mitre.org/techniques/T1078/
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
      - AADNonInteractiveUserSignInLogs
tactics:
  - CredentialAccess
  - InitialAccess
relevantTechniques:
  - T1110.003
  - T1078
query: |
  let starttime = todatetime('{{StartTimeISO}}');
  let endtime = todatetime('{{EndTimeISO}}');
  let correlationWindow = 15m;
  let minFailedUsersFromIP = 5;
  let minFailedAttemptsFromIP = 20;
  let maxSuccessfulUsersFromIP = 3;
  let successCodes = dynamic(["0", "50125", "50140", "70043", "70044"]);
  let signins =
      union isfuzzy=true
      (
          SigninLogs
          | where TimeGenerated between (starttime .. endtime)
          | project
              TimeGenerated,
              SourceTable = "SigninLogs",
              UserPrincipalName = tolower(UserPrincipalName),
              IPAddress,
              ResultType = tostring(ResultType),
              ResultDescription,
              AppDisplayName,
              ClientAppUsed,
              CorrelationId
      ),
      (
          AADNonInteractiveUserSignInLogs
          | where TimeGenerated between (starttime .. endtime)
          | project
              TimeGenerated,
              SourceTable = "AADNonInteractiveUserSignInLogs",
              UserPrincipalName = tolower(UserPrincipalName),
              IPAddress,
              ResultType = tostring(ResultType),
              ResultDescription,
              AppDisplayName,
              ClientAppUsed,
              CorrelationId
      )
      | where isnotempty(UserPrincipalName) and isnotempty(IPAddress);
  let fail

Required Data Sources

Sentinel TableNotes
AADNonInteractiveUserSignInLogsEnsure this data connector is enabled
SigninLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/MultipleDataSources/IPIdentityFailureBurstFollowedBySuccess.yaml