Query Details

RULE 02 SP Brute Force Authentication

Query

// Rule    : Workload Identity - Service Principal Brute Force Authentication
// Severity: High
// Tactics : CredentialAccess
// MITRE   : T1110.003 (Brute Force: Password Spraying), T1078.004
// Freq    : PT1H   Period: PT1H
//==========================================================================================

// --- Successful sign-ins in same window (for corroboration) ---
// ---- 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 SuccessfulSignins = (AADServicePrincipalSignInLogs | invoke _ExcludeAllowlistedIPs())
    | where TimeGenerated > ago(1h)
    | where ResultType == "0"
    | summarize SuccessCount = count(), SuccessIPs = make_set(IPAddress, 5)
        by ServicePrincipalId;
// --- Failed authentications ---
(AADServicePrincipalSignInLogs | invoke _ExcludeAllowlistedIPs())
| where TimeGenerated > ago(1h)
| where ResultType != "0"
| where ResultType !in ("50076", "50079", "50058", "70044")   // exclude MFA prompts, session expiry (FP)
| summarize
    FailedCount      = count(),
    UniqueIPs        = dcount(IPAddress),
    IPList           = make_set(IPAddress, 20),
    ErrorCodes       = make_set(ResultType, 10),
    ErrorDescriptions = make_set(ResultDescription, 5),
    Resources        = make_set(ResourceDisplayName, 10),
    CredTypes        = make_set(ClientCredentialType, 5),
    FirstAttempt     = min(TimeGenerated),
    LastAttempt      = max(TimeGenerated)
    by ServicePrincipalName, ServicePrincipalId, AppId
| where FailedCount > 20 or UniqueIPs > 5
| join kind=leftouter SuccessfulSignins on ServicePrincipalId
| extend
    SpraySucceeded   = SuccessCount > 0,
    Severity         = case(
        SuccessCount > 0 and FailedCount > 20, "Critical",
        FailedCount > 50,                      "High",
        "Medium")

Explanation

This query is designed to detect potential brute force attacks on service principals by analyzing authentication logs. Here's a simplified breakdown of what the query does:

  1. Allowlist Setup: It first sets up a list of trusted IP addresses or ranges (allowlist) to exclude them from further analysis. This helps focus on potentially malicious activity from untrusted sources.

  2. Successful Sign-ins: It checks for successful sign-ins from untrusted IPs within the last hour. It counts these successful attempts and notes the IPs involved, grouping them by the service principal ID.

  3. Failed Authentications: It then looks for failed authentication attempts from untrusted IPs within the same time frame. It excludes certain error codes related to multi-factor authentication prompts and session expiries to avoid false positives.

  4. Summary of Failed Attempts: For each service principal, it summarizes the number of failed attempts, the number of unique IPs involved, and other details like error codes, resources accessed, and credential types used. It also records the time of the first and last failed attempt.

  5. Suspicious Activity Detection: It flags service principals with more than 20 failed attempts or more than 5 unique IPs involved. It then checks if any of these principals also had successful sign-ins, which could indicate a successful brute force attack.

  6. Severity Assignment: Based on the number of failed attempts and whether any succeeded, it assigns a severity level to the potential threat: "Critical" if there are successful sign-ins after many failed attempts, "High" if there are more than 50 failed attempts, and "Medium" otherwise.

Overall, this query helps identify and prioritize potential security threats related to brute force attacks on service principals by analyzing authentication patterns and excluding trusted sources.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AADServicePrincipalSignInLogs

Keywords

WorkloadIdentityServicePrincipalAuthenticationNetworkIPsLogsTimeResultErrorResourcesApp

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listnotipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awayinvokeagocountmake_setdcountminmaxjoinkindleftoutercase

Actions