Query Details

HUNT 17 Risk Event SP Escalation Timeline 30d

Query

// Hunt    : Workload Identity - User Risk Event Timeline Correlated with SP Privilege Changes (30d)
// Purpose : Builds a chronological timeline pairing each AADUserRiskEvents entry with all
//           SP privilege escalation or credential operations performed by the same user within
//           72 hours before or after the risk event. Use to reconstruct the attack chain:
//           what was done to workload identities before, during, and after account compromise.
//           Includes time-delta annotations to identify suspicious proximity between risk
//           detection and SP modification.
// Tables  : AADUserRiskEvents, AuditLogs
// Period  : P30D
// Tactics : CredentialAccess, Persistence, PrivilegeEscalation, LateralMovement
// MITRE   : T1098.001, T1078.004, T1098.003
//==========================================================================================

let LookbackDays   = 30d;
let ProximityHours = 72;

let HighImpactOps = dynamic([
    "Add service principal credentials",
    "Remove service principal credentials",
    "Add password to service principal",
    "Add key credential to service principal",
    "Add owner to service principal",
    "Add owner to application",
    "Add app role assignment to service principal",
    "Add delegated permission grant",
    "Consent to application",
    "Add OAuth2PermissionGrant",
    "Add member to role",
    "Add application",
    "Add service principal"
]);

// All user risk events in the window
let RiskEvents = union isfuzzy=true
    AADUserRiskEvents,
    (datatable(UserId:string, UserPrincipalName:string, RiskEventType:string,
               RiskLevel:string, DetectedDateTime:datetime)[])
    | where DetectedDateTime > ago(LookbackDays)
    | project
        UserId,
        UserPrincipalName,
        RiskEventType,
        RiskLevel,
        RiskDetected = DetectedDateTime;

// SP lifecycle operations in the same window
let SPOps = AuditLogs
    | where TimeGenerated > ago(LookbackDays)
    | where OperationName has_any (HighImpactOps)
    | where Result =~ "success"
    | where isnotempty(tostring(InitiatedBy.user.id))
    | extend InitiatorUserId = tostring(InitiatedBy.user.id)
    | extend InitiatorUPN    = tostring(InitiatedBy.user.userPrincipalName)
    | extend TargetId        = tostring(TargetResources[0].id)
    | extend TargetName      = tostring(TargetResources[0].displayName)
    | extend InitiatorIP     = tostring(InitiatedBy.user.ipAddress)
    | project
        InitiatorUserId, InitiatorUPN, OperationName,
        TargetId, TargetName, InitiatorIP,
        AuditTime = TimeGenerated;

// Cross-join and compute time delta between risk detection and SP operation
RiskEvents
| join kind=inner SPOps on $left.UserId == $right.InitiatorUserId
| extend DeltaHours = datetime_diff("hour", AuditTime, RiskDetected)
| where abs(DeltaHours) <= ProximityHours
| extend Proximity = case(
    DeltaHours < 0,  strcat("Before risk by ", tostring(abs(DeltaHours)), "h"),
    DeltaHours == 0, "Same hour as risk detection",
    strcat("After risk by ", tostring(DeltaHours), "h"))
| extend Phase = case(
    DeltaHours < -24,   "Pre-detection (>24h before)",
    DeltaHours < 0,     "Pre-detection (<24h before)",
    DeltaHours == 0,    "Simultaneous",
    DeltaHours <= 6,    "Post-detection (<6h)",
    DeltaHours <= 24,   "Post-detection (6-24h)",
    "Post-detection (24-72h)")
| project
    UserPrincipalName, UserId, RiskEventType, RiskLevel, RiskDetected,
    OperationName, TargetName, TargetId, InitiatorIP,
    AuditTime, DeltaHours, Proximity, Phase
| order by UserPrincipalName asc, AuditTime asc

Explanation

This query is designed to help security analysts understand potential security incidents involving Azure Active Directory (AAD) users and service principals (SPs). Here's a simplified breakdown of what the query does:

  1. Purpose: The query aims to create a timeline that links user risk events with any privilege changes or credential operations on service principals performed by the same user within a 72-hour window before or after the risk event. This helps in reconstructing the sequence of actions taken during a potential account compromise.

  2. Data Sources: It uses two main data sources:

    • AADUserRiskEvents: Logs of risk events associated with AAD users.
    • AuditLogs: Logs of operations performed on service principals.
  3. Time Frame: The query looks at data from the last 30 days.

  4. Operations of Interest: It focuses on specific high-impact operations related to service principals, such as adding or removing credentials, adding roles, or granting permissions.

  5. Process:

    • It first collects all user risk events from the past 30 days.
    • It then gathers all successful service principal operations within the same period that match the high-impact operations list.
    • The query joins these two datasets to find instances where the same user is involved in both a risk event and a service principal operation within a 72-hour window.
    • It calculates the time difference between the risk event and the service principal operation to determine if the operation happened before, during, or after the risk event.
  6. Output: The result is a timeline that shows:

    • The user's details and the type of risk event.
    • The specific operation performed on the service principal.
    • The time difference between the risk event and the operation.
    • A classification of the operation's timing relative to the risk event (e.g., "Before risk by X hours", "Simultaneous", "After risk by X hours").
  7. Use Case: This timeline helps analysts identify suspicious activities and understand the sequence of events in potential security incidents, aiding in the investigation and response process.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

AADUserRiskEventsAuditLogs

Keywords

DevicesIntuneUserServicePrincipalApplicationRiskEventsAuditLogs

Operators

letdynamicunionisfuzzydatatablewhereagoprojecthas_any=~isnotemptytostringextendjoinkindon==datetime_diffabscasestrcatorder byasc

Actions