Query Details

HUNT 13 SP Risk Events Attribution 30d

Query

// Hunt     : Workload Identity - SP Risk Events Attribution and Attack Reconstruction (30d)
// Tactics  : CredentialAccess, Persistence, InitialAccess
// MITRE    : T1078.004, T1550.001
// Purpose  : Full 30-day attribution of ServicePrincipalRiskEvents. Identifies SPs with
//            the most varied risk event types (broad attack surface) and computes a
//            composite attack-breadth score. Reconstructs the exploitation picture by
//            correlating with concurrent sign-in activity and credential/privilege changes
//            to support DFIR triage and incident response.
//==========================================================================================

let LookbackDays = 30d;
let HighSeverityTypes = dynamic([
    "investigationsThreatIntelligence",
    "anomalousToken",
    "maliciousIPAddress",
    "suspiciousAPITraffic"
]);

// --- isfuzzy=true: table only exists when Identity Protection for Workload Identities is licensed ---
let _SPRiskEvents = union isfuzzy=true
    ServicePrincipalRiskEvents,
    (datatable(TimeGenerated:datetime, ServicePrincipalId:string, ServicePrincipalDisplayName:string,
               AppId:string, RiskEventType:string, RiskLevel:string, DetectedDateTime:datetime)[]);

// --- Per-SP risk event summary with composite risk scoring ---
let PerSPSummary = _SPRiskEvents
    | where TimeGenerated > ago(LookbackDays)
    | extend RiskScore = case(
        RiskLevel == "high",   3,
        RiskLevel == "medium", 2,
        RiskLevel == "low",    1,
        0)
    | extend IsHighSeverity = RiskEventType in (HighSeverityTypes)
    | summarize
        TotalEvents       = count(),
        UniqueEventTypes  = dcount(RiskEventType),
        EventTypes        = make_set(RiskEventType, 15),
        HighSeverityCount = countif(IsHighSeverity),
        MaxRiskScore      = max(RiskScore),
        TotalRiskScore    = sum(RiskScore),
        FirstEvent        = min(DetectedDateTime),
        LastEvent         = max(DetectedDateTime)
        by ServicePrincipalId, ServicePrincipalDisplayName, AppId;

// --- Sign-in activity for the same SPs over the lookback period ---
let SigninActivity = (AADServicePrincipalSignInLogs | invoke ExcludeAllowlistedIPs())
    | where TimeGenerated > ago(LookbackDays)
    | summarize
        TotalSignins      = count(),
        SuccessfulSignins = countif(ResultType == "0"),
        UniqueCountries   = dcount(Location),
        Countries         = make_set(Location, 10),
        UniqueIPs         = dcount(IPAddress),
        Resources         = make_set(ResourceDisplayName, 10),
        LastSignin        = max(TimeGenerated)
        by ServicePrincipalId;

// --- Credential and privilege changes proximate to risk events ---
let CredChanges = AuditLogs
    | where TimeGenerated > ago(LookbackDays)
    | where OperationName in (
        "Add service principal credentials",
        "Update service principal",
        "Add app role assignment to service principal",
        "Add delegated permission grant")
    | where Result =~ "success"
    | extend SPId = tostring(TargetResources[0].id)
    | summarize
        CredChangeCount = count(),
        CredOps         = make_set(OperationName, 5),
        LastCredChange  = max(TimeGenerated)
        by SPId;

PerSPSummary
| join kind=leftouter SigninActivity on ServicePrincipalId
| join kind=leftouter CredChanges on $left.ServicePrincipalId == $right.SPId
| extend MaxRiskLevel = case(
    MaxRiskScore == 3, "high",
    MaxRiskScore == 2, "medium",
    MaxRiskScore == 1, "low",
    "none")
| extend AttackBreadth = case(
    UniqueEventTypes >= 4, "Very broad — 4+ detection types triggered",
    UniqueEventTypes == 3, "Broad — 3 detection types triggered",
    UniqueEventTypes == 2, "Moderate — 2 detection types triggered",
    "Narrow — single detection type triggered")
| extend InvestigationPriority = case(
    HighSeverityCount >= 1 and coalesce(CredChangeCount, 0) > 0,    "1-Critical — Threat intel detection with credential changes",
    HighSeverityCount >= 2,                                           "2-Critical — Multiple high-severity risk event types",
    HighSeverityCount >= 1,                                           "3-High — High-severity risk event type",
    UniqueEventTypes >= 3 and coalesce(CredChangeCount, 0) > 0,     "4-High — Risk stacking with credential changes",
    UniqueEventTypes >= 3,                                            "5-High — 3+ distinct risk event types",
    UniqueEventTypes >= 2,                                            "6-Medium — Multiple risk event types",
    "7-Low — Single risk event type for baselining")
| project
    ServicePrincipalDisplayName, ServicePrincipalId, AppId,
    TotalEvents, UniqueEventTypes, EventTypes, MaxRiskLevel, TotalRiskScore,
    HighSeverityCount, AttackBreadth,
    TotalSignins, SuccessfulSignins, UniqueIPs, Countries, Resources, LastSignin,
    CredChangeCount, CredOps, LastCredChange,
    InvestigationPriority, FirstEvent, LastEvent
| order by TotalRiskScore desc, HighSeverityCount desc

Explanation

This KQL query is designed to analyze and attribute risk events related to service principals (SPs) over the past 30 days. Here's a simplified breakdown of what the query does:

  1. Purpose: The query aims to identify service principals with diverse risk event types, calculate a composite risk score, and reconstruct the attack scenario by correlating risk events with sign-in activities and credential changes. This helps in digital forensics and incident response (DFIR).

  2. Data Sources:

    • ServicePrincipalRiskEvents: Collects risk events associated with service principals.
    • AADServicePrincipalSignInLogs: Tracks sign-in activities of service principals.
    • AuditLogs: Monitors credential and privilege changes related to service principals.
  3. Risk Event Analysis:

    • Filters risk events from the last 30 days.
    • Assigns a risk score based on the severity of the risk level (high, medium, low).
    • Identifies high-severity events from a predefined list.
    • Summarizes data per service principal, including total events, unique event types, high-severity event count, and risk scores.
  4. Sign-in Activity:

    • Analyzes sign-in logs for the same service principals over the last 30 days.
    • Summarizes total sign-ins, successful sign-ins, unique countries and IPs, and resources accessed.
  5. Credential Changes:

    • Examines audit logs for credential and privilege changes related to service principals.
    • Summarizes the number of changes and the types of operations performed.
  6. Data Correlation and Scoring:

    • Joins the summarized risk events, sign-in activities, and credential changes.
    • Calculates the maximum risk level and attack breadth based on unique event types.
    • Determines an investigation priority score based on high-severity events and credential changes.
  7. Output:

    • Projects relevant information such as service principal display name, total events, risk scores, sign-in details, credential changes, and investigation priority.
    • Orders the results by total risk score and high-severity count to prioritize investigation.

This query helps security teams quickly identify and prioritize service principals that may be at risk or have been compromised, facilitating efficient incident response.

Details

David Alonso profile picture

David Alonso

Released: April 21, 2026

Tables

ServicePrincipalRiskEventsAADServicePrincipalSignInLogsAuditLogs

Keywords

ServicePrincipalRiskEventsServicePrincipalDisplayNameAppIdRiskEventTypeRiskLevelDetectedDateTimeAADServicePrincipalSignInLogsExcludeAllowlistedIPsResultTypeLocationIPAddressResourceDisplayNameAuditLogsOperationNameTargetResources

Operators

letuniondatatablewhereextendcaseinsummarizecountdcountmake_setcountifmaxsummininvokejoinkindoncoalesceprojectorder by

Actions