Query Details

RULE 06 MI Lateral Movement Resource Spread

Query

// Rule    : Workload Identity - Managed Identity Lateral Movement (High Resource Spread)
// Severity: High
// Tactics : LateralMovement, CredentialAccess, Discovery
// MITRE   : T1550, T1552.001, T1087
// Freq    : PT1H   Period: PT1H
//==========================================================================================

// ---- 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 SensitiveResourceKeywords = dynamic([
    "Key Vault", "Storage", "SQL", "Cognitive", "OpenAI", "Synapse",
    "Service Bus", "Event Hub", "Container Registry", "Kubernetes"
]);

// --- isfuzzy=true + datatable fallback: deployment succeeds even if AADManagedIdentitySignInLogs is not yet enabled ---
let MILogs = union isfuzzy=true
    (AADManagedIdentitySignInLogs | invoke _ExcludeAllowlistedIPs()),
    (datatable(TimeGenerated:datetime, ServicePrincipalId:string, ServicePrincipalName:string,
               ResourceDisplayName:string, SourceAppClientId:string, ResultType:string)[]);
// --- Baseline: max unique sensitive resources accessed per MI per hour (14d) ---
let Baseline = MILogs
    | where TimeGenerated between (ago(14d) .. ago(1h))
    | where ResultType == "0"
    | where ResourceDisplayName has_any (SensitiveResourceKeywords)
    | summarize HourlyResources = dcount(ResourceDisplayName)
        by ServicePrincipalId, bin(TimeGenerated, 1h)
    | summarize
        MaxHourlyResources = max(HourlyResources),
        AvgHourlyResources = avg(HourlyResources)
        by ServicePrincipalId;

MILogs
| where TimeGenerated > ago(1h)
| where ResultType == "0"
| where ResourceDisplayName has_any (SensitiveResourceKeywords)
| summarize
    AuthCount        = count(),
    UniqueResources  = dcount(ResourceDisplayName),
    Resources        = make_set(ResourceDisplayName, 20),
    SourceApps       = make_set(SourceAppClientId, 10),
    UniqueSourceApps = dcount(SourceAppClientId),
    FirstActivity    = min(TimeGenerated),
    LastActivity     = max(TimeGenerated)
    by ServicePrincipalName, ServicePrincipalId
| where UniqueResources >= 5 or AuthCount > 200
| join kind=leftouter Baseline on ServicePrincipalId
| extend
    ExceedsBaseline  = UniqueResources > MaxHourlyResources * 1.5,
    SpreadScore      = UniqueResources

Explanation

This query is designed to detect potential security threats related to managed identities in a cloud environment, specifically focusing on lateral movement, credential access, and discovery tactics. Here's a simplified breakdown of what the query does:

  1. Network Allowlist: It sets up a list of trusted IP addresses or ranges to exclude them from further analysis. This helps in focusing only on potentially suspicious activities.

  2. Sensitive Resources: It defines a list of keywords representing sensitive resources, such as "Key Vault", "Storage", "SQL", etc., which are of particular interest when accessed.

  3. Log Collection: It gathers logs of managed identity sign-ins, ensuring that even if some logs are not enabled, the query can still run using a fallback mechanism.

  4. Baseline Establishment: It calculates a baseline for the maximum and average number of unique sensitive resources accessed per managed identity per hour over the past 14 days. This helps in identifying unusual access patterns.

  5. Current Activity Analysis: It analyzes managed identity sign-ins from the last hour, focusing on successful accesses to sensitive resources. It summarizes the data by counting the number of authentications, unique resources accessed, and source applications involved.

  6. Anomaly Detection: It identifies cases where a managed identity accessed five or more unique resources or had more than 200 authentication attempts in the last hour. It then checks if this activity exceeds the established baseline by more than 1.5 times.

  7. Output: The query outputs information about managed identities that show unusual access patterns, potentially indicating a security threat. This includes the number of unique resources accessed, the spread score, and whether the activity exceeds the baseline.

Overall, this query helps in identifying and investigating potential security incidents involving managed identities accessing sensitive resources in a cloud environment.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AADManagedIdentitySignInLogs

Keywords

WorkloadIdentityManagedIdentityNetworkIPsResourcesServicePrincipalLogsActivity

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptytoscalarmatchesregexextendiffhasstrcatsummarizemake_listnotipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointisnullproject-awaydynamicinvokedatatablebetweenagobinavgbycountdcountmake_setminmaxjoinkindleftouteronextend

Actions