← Rule library
medium KQL Entra ID
Password Spray Against Entra ID
Low-and-slow failures across many accounts from few source IPs - tuned on distinct-user count rather than raw failure volume.
Rule source
// Password Spray Against Entra ID
// T1110.003 - Brute Force: Password Spraying
// Tuned on distinct-user count, not raw failure volume. Spraying stays
// shallow per account - that shallowness is the actual signature.
let window = 30m;
let min_users = 15;
let max_depth = 3.0;
SigninLogs
| where TimeGenerated > ago(window)
| where ResultType in ("50126", "50053", "50055") // bad password / locked / expired
| summarize
FailedUsers = dcount(UserPrincipalName),
Attempts = count(),
UserSample = make_set(UserPrincipalName, 12),
Apps = make_set(AppDisplayName, 5),
FirstSeen = min(TimeGenerated),
LastSeen = max(TimeGenerated)
by IPAddress, Country = tostring(LocationDetails.countryOrRegion)
| where FailedUsers >= min_users
| extend AttemptsPerUser = round(todouble(Attempts) / FailedUsers, 2)
| where AttemptsPerUser <= max_depth
| order by FailedUsers desc Required telemetry
Entra ID SigninLogs, non-interactive included
Confirm this source is arriving before you judge the rule. A detection built on telemetry you do not collect looks identical to one that simply never fires.
Known false positives
- A broken service account looping against many mailboxes
- Post-migration clients holding stale credentials
Validation
Replay a historic spray window and confirm the threshold still fires
Re-run quarterly. Rules decay because the environment moves underneath them - agent upgrades, schema changes and config edits all break detections silently.