Query Details

RULE 10 SP CA Bypass Management Ops

Query

// Rule    : Azure - Service Principal CA Bypass Sign-in Followed by Management Operations
// Severity: High
// Tactics : InitialAccess, CredentialAccess
// MITRE   : T1528, T1078
// Freq    : PT1H   Period: PT2H
//==========================================================================================

let CABypassSignins = AADServicePrincipalSignInLogs
    | where TimeGenerated > ago(2h)
    | where ResultType == 0
    | where ConditionalAccessStatus has_any ("failure", "notApplied")
    | where isnotempty(IPAddress)
    | where IPAddress !startswith "168.63." and IPAddress !startswith "169.254."
    | project SigninTime = TimeGenerated, ServicePrincipalId, ServicePrincipalName, SigninIP = IPAddress, AppId, ConditionalAccessStatus;
AzureActivity
| where TimeGenerated > ago(2h)
| where ActivityStatusValue =~ "Success"
| where OperationNameValue has_any ("WRITE", "DELETE", "ACTION")
| where not(OperationNameValue has_any ("READ", "LIST", "GET", "LISTKEYS"))
| where isnotempty(CallerIpAddress)
| join kind=inner CABypassSignins on $left.CallerIpAddress == $right.SigninIP
| where TimeGenerated > SigninTime and TimeGenerated <= SigninTime + 1h
| summarize
    MGMTOpCount = count(),
    Operations = make_set(OperationNameValue, 10),
    AffectedResources = make_set(ResourceId, 10),
    SubscriptionIds = make_set(SubscriptionId, 5),
    CallerIP = any(CallerIpAddress),
    CAStatus = any(ConditionalAccessStatus),
    FirstMGMTOp = min(TimeGenerated),
    LastMGMTOp = max(TimeGenerated)
    by Caller, ServicePrincipalName, SigninTime
| where MGMTOpCount >= 2
| extend
    AccountName = coalesce(tostring(split(Caller, "@")[0]), ServicePrincipalName),
    AccountUPNSuffix = tostring(split(Caller, "@")[1])

Explanation

This query is designed to detect suspicious activity involving Azure Service Principals. Here's a simplified explanation:

  1. Objective: The query aims to identify instances where a Service Principal in Azure bypasses Conditional Access (CA) policies during sign-in and then performs management operations, which could indicate unauthorized access or misuse.

  2. Data Sources:

    • AADServicePrincipalSignInLogs: This log contains records of sign-ins by Service Principals.
    • AzureActivity: This log records management operations performed in Azure.
  3. Steps:

    • Identify CA Bypass Sign-ins: The query first filters sign-in logs from the past 2 hours to find instances where a Service Principal successfully signed in (ResultType == 0) but did not fully comply with Conditional Access policies (status was "failure" or "notApplied"). It excludes sign-ins from certain internal IP ranges.
    • Identify Management Operations: It then looks for successful management operations (like WRITE, DELETE, ACTION) in the AzureActivity logs within the same 2-hour window. It excludes operations that are typically read-only (READ, LIST, GET, LISTKEYS).
    • Correlate Sign-ins with Operations: The query joins these two datasets on the IP address used during sign-in and the management operation. It checks if the operations occurred within 1 hour after the sign-in.
    • Summarize and Filter: It summarizes the data to count the number of operations, list the types of operations, affected resources, and subscriptions. It only considers cases where at least 2 management operations were performed.
  4. Output:

    • The query outputs details such as the Service Principal name, the number of operations, types of operations, affected resources, and the time range of these operations.
    • It also extracts the account name and domain from the Caller information.
  5. Use Case: This query is useful for security teams to detect and investigate potential security incidents involving Service Principals that might be bypassing security controls to perform unauthorized actions in Azure.

Details

David Alonso profile picture

David Alonso

Released: March 12, 2026

Tables

AADServicePrincipalSignInLogsAzureActivity

Keywords

AzureActivityServicePrincipalSigninIPAddressOperationNameResourceIDSubscriptionIDCallerConditionalAccessStatusAccountNameAccountUPNSuffix

Operators

let|where>ago()==has_any()isnotempty()!startswithproject=~not()joinon==summarizecount()make_set()any()min()max()by>=extendcoalesce()tostring()split()

Actions