Query Details

RULE 04 SP CA Bypass Cloud App Events

Query

// Rule    : Workload Identity - SP Conditional Access Bypass → Cloud App Events Correlation
// Severity: High
// Tactics : InitialAccess, Exfiltration
// MITRE   : T1078.004, T1567, T1530
// Freq    : PT1H   Period: PT2H
//==========================================================================================

// ---- 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 PrivateRanges = dynamic(["10.", "192.168.", "172.16.", "172.17.", "172.18.",
    "172.19.", "172.20.", "172.21.", "172.22.", "172.23.", "172.24.", "172.25.",
    "172.26.", "172.27.", "172.28.", "172.29.", "172.30.", "172.31.",
    "127.", "169.254.", "168.63."]);
let HighRiskActions = dynamic([
    "FileDownloaded", "FileSyncDownloadedFull", "FileDeleted", "FileMoved",
    "SendAs", "MailItemsAccessed", "MassDelete", "DataExfiltration"
]);
// --- SPs that bypassed CA in the last 2 hours ---
let CABypassSPs = (AADServicePrincipalSignInLogs | invoke _ExcludeAllowlistedIPs())
    | where TimeGenerated > ago(2h)
    | where ResultType == "0"
    | where ConditionalAccessStatus in ("notApplied", "failure")
    | where isnotempty(IPAddress)
    | where not(IPAddress has_any (PrivateRanges))
    | project SigninTime = TimeGenerated, ServicePrincipalId, ServicePrincipalName,
        AppId, SigninIP = IPAddress, CAStatus = ConditionalAccessStatus,
        Resource = ResourceDisplayName;
// --- Subsequent high-risk cloud app activity from same SP within 1h ---
// union isfuzzy=true + datatable fallback: deployment succeeds even if CloudAppEvents (M365D connector) is not enabled
union isfuzzy=true
    (CloudAppEvents | invoke _ExcludeAllowlistedIPs()),
    (datatable(TimeGenerated:datetime, ActionType:string, AccountObjectId:string, ObjectName:string, Application:string)[])
| where TimeGenerated > ago(2h)
| where ActionType in (HighRiskActions)
| join kind=inner CABypassSPs on $left.AccountObjectId == $right.ServicePrincipalId
| where TimeGenerated > SigninTime and TimeGenerated <= SigninTime + 1h
| summarize
    ActionCount      = count(),
    Actions          = make_set(ActionType, 10),
    AffectedObjects  = make_set(ObjectName, 20),
    Applications     = make_set(Application, 5),
    SigninIP         = any(SigninIP),
    CAStatus         = any(CAStatus),
    Resource         = any(Resource),
    SigninTime       = any(SigninTime),
    FirstAction      = min(TimeGenerated),
    LastAction       = max(TimeGenerated)
    by ServicePrincipalName, ServicePrincipalId, AppId
| where ActionCount >= 2

Explanation

This KQL query is designed to detect potentially malicious activity involving service principals (SPs) in a cloud environment. Here's a simplified breakdown of what the query does:

  1. Network Allowlist: It defines a list of trusted IP addresses and ranges that should be excluded from analysis. This is done to focus on potentially suspicious activity from untrusted sources.

  2. Private IP Ranges: It specifies a list of private IP address ranges that are typically used within internal networks and are not considered suspicious.

  3. High-Risk Actions: It identifies a set of actions considered high-risk, such as file downloads, deletions, and data exfiltration.

  4. Identify SPs Bypassing Conditional Access: The query looks for service principals that have bypassed conditional access policies within the last two hours. It filters out any sign-ins from trusted IPs and private ranges, focusing on those that resulted in a "notApplied" or "failure" status for conditional access.

  5. Correlate with High-Risk Activity: It then checks if these service principals performed any high-risk actions within one hour after bypassing conditional access. This is done by joining the bypassed SPs with cloud app events that include high-risk actions.

  6. Summarize Findings: The query summarizes the findings by counting the number of high-risk actions, listing the types of actions, affected objects, applications involved, and other relevant details. It only reports cases where at least two high-risk actions were detected.

Overall, this query is used to identify and investigate potential security incidents where service principals might be used to bypass security controls and perform unauthorized or risky operations in the cloud environment.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AADServicePrincipalSignInLogsCloudAppEvents

Keywords

CloudAppEventsServicePrincipalIPAddressResourceApplicationActionTypeSigninTime

Operators

letmaterializeunionisfuzzyprinttakeprojecttostringwhereisnotemptytoscalarmatchesregexextendiffstrcatsummarizemake_listnotarray_lengthisnullipv4_is_in_any_rangemv-applytotypeofsplitipv4_comparemaxtointproject-awaydynamicagoinhas_anyinvokejoinkindonsummarizecountmake_setanyminmaxby

Actions