// Normalized SigninEvents; illustrative parameters, not calibrated production settings. let Window = 15m; let Followup = 30m; let MinFailures = 6; let MinAccounts = 3; let MinSources = 2; let Events = SigninEvents | where isnotempty(TenantId) and isnotempty(UserId) and isnotempty(EventId) | summarize arg_max(TimeGenerated, *) by TenantId, EventId; let Failures = Events | where ResultType == "50126" and isnotempty(IPAddress) | extend WindowStart = bin(TimeGenerated, Window); let Bursts = Failures | summarize Failures=count(), Accounts=count_distinct(UserId), Sources=count_distinct(IPAddress) by TenantId, WindowStart | where Failures >= MinFailures and Accounts >= MinAccounts and Sources >= MinSources; let Attempted = Failures | summarize FirstFailure=min(TimeGenerated), LastFailure=max(TimeGenerated) by TenantId, UserId, WindowStart | join kind=inner Bursts on TenantId, WindowStart; Attempted | join kind=inner (Events | where ResultType == "0" | project TenantId, UserId, SuccessTime=TimeGenerated, SuccessEventId=EventId, SuccessIP=IPAddress) on TenantId, UserId | where SuccessTime > LastFailure and SuccessTime <= LastFailure + Followup | project TenantId, UserId, WindowStart, FirstFailure, LastFailure, SuccessTime, SuccessEventId, SuccessIP, Failures, Accounts, Sources | order by TenantId asc, UserId asc, SuccessTime asc