Query Details

RULE 12 SP Signin Volume Anomaly

Query

// Rule    : Workload Identity - SP Sign-in Anomaly Spike (Statistical Baseline Deviation)
// Severity: Medium
// Tactics : Discovery, CredentialAccess
// MITRE   : T1078.004, T1087.004
// Freq    : PT1H   Period: P14D
//==========================================================================================

// ---- 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 DeviationThreshold = 3.0;    // 3 standard deviations above mean
let MinSigninsThreshold = 50;    // minimum sign-ins in current hour
let MinBaselineDays     = 3;

// --- Baseline: per-SP hourly sign-in counts (last 13 days) ---
let Baseline = (AADServicePrincipalSignInLogs | invoke _ExcludeAllowlistedIPs())
    | where TimeGenerated between (ago(14d) .. ago(1h))
    | where ResultType == "0"
    | summarize HourlySignins = count()
        by ServicePrincipalId, bin(TimeGenerated, 1h)
    | summarize
        AvgPerHour  = avg(HourlySignins),
        StdDevPerHour = stdev(HourlySignins),
        SampleDays  = dcount(bin(TimeGenerated, 1d))
        by ServicePrincipalId
    | where SampleDays >= MinBaselineDays;

// --- Current 1-hour window ---
let CurrentWindow = (AADServicePrincipalSignInLogs | invoke _ExcludeAllowlistedIPs())
    | where TimeGenerated > ago(1h)
    | where ResultType == "0"
    | summarize
        CurrentSignins   = count(),
        UniqueResources  = dcount(ResourceDisplayName),
        Resources        = make_set(ResourceDisplayName, 10),
        UniqueIPs        = dcount(IPAddress),
        CredTypes        = make_set(ClientCredentialType, 5),
        FirstSignin      = min(TimeGenerated),
        LastSignin       = max(TimeGenerated)
        by ServicePrincipalName, ServicePrincipalId, AppId;

CurrentWindow
| join kind=inner Baseline on ServicePrincipalId
| where CurrentSignins >= MinSigninsThreshold
| extend DeviationScore = iff(
    StdDevPerHour > 0,
    (CurrentSignins - AvgPerHour) / StdDevPerHour,
    toreal(CurrentSignins))
| where DeviationScore >= DeviationThreshold
    or (AvgPerHour < 5 and CurrentSignins >= MinSigninsThreshold)
| project
    ServicePrincipalName, ServicePrincipalId, AppId,
    CurrentSignins, AvgPerHour = round(AvgPerHour, 1),
    StdDevPerHour = round(StdDevPerHour, 1),
    DeviationScore = round(DeviationScore, 1),
    UniqueResources, Resources, UniqueIPs, CredTypes,
    FirstSignin, LastSignin

Explanation

This query is designed to detect unusual spikes in sign-in activity for service principals (SPs) in Azure Active Directory, which could indicate potential security threats such as unauthorized access or credential misuse. Here's a simplified breakdown of what the query does:

  1. Exclude Trusted IPs: It first defines a list of trusted IP addresses or ranges (allowlist) and excludes any sign-ins coming from these trusted sources. This helps focus the analysis on potentially suspicious activity.

  2. Set Thresholds: It establishes thresholds for detecting anomalies:

    • A deviation threshold of 3 standard deviations above the mean.
    • A minimum of 50 sign-ins in the current hour to consider the data significant.
    • A baseline period of at least 3 days of historical data.
  3. Calculate Baseline: It calculates a baseline of hourly sign-in counts for each service principal over the past 13 days, excluding the current hour. This baseline includes the average and standard deviation of sign-ins per hour.

  4. Analyze Current Activity: It examines the sign-in activity for the current hour, capturing details such as the number of sign-ins, unique resources accessed, unique IP addresses, credential types used, and the time range of sign-ins.

  5. Detect Anomalies: It compares the current hour's sign-in activity against the baseline:

    • If the number of sign-ins significantly deviates from the baseline (more than 3 standard deviations above the mean), or
    • If the baseline average is low (less than 5 sign-ins per hour) but the current sign-ins are high (at least 50), it flags this as an anomaly.
  6. Output Results: It outputs details of the service principals with anomalous sign-in activity, including the deviation score, which indicates how much the current activity deviates from the norm.

This query helps identify potential security incidents by highlighting unusual sign-in patterns that could signify unauthorized access attempts or other malicious activities.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AADServicePrincipalSignInLogs

Keywords

ServicePrincipalSigninIPAddressResourceCredentialTimeGenerated

Operators

letmaterializeunionisfuzzyprinttake_GetWatchlistprojecttostringwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awaybetweenagobinavgstdevdcountcountinvokejoinkindinnerroundorandnotonbytorealminmaxmake_setproject

Actions