Part 5 - Advanced Phishing Detection and Response with Microsoft Sentinel and the Unified SOC

This is Part 5 of our six-part series on phishing attacks and defences. Part 1 provided an overview of the series, Part 2 explored various phishing attack types, Part 3 examined advanced phishing frameworks, our bonus content delved into post-exploitation techniques, and Part 4 covered comprehensive protection strategies.

Despite our best preventative efforts, sophisticated phishing attacks will occasionally succeed. When prevention fails, rapid detection becomes critical to limiting the impact of a compromise. Microsoft Sentinel and the new Unified Security Operations Platform provide powerful capabilities for identifying potentially phished users and orchestrating swift responses to contain and remediate threats.

In this fifth instalment, we'll explore how to leverage Microsoft Sentinel within the Unified SOC to detect and respond to phishing attacks, with a special focus on detecting modern attack techniques like OAuth abuse, token theft, and Adversary-in-the-Middle (AiTM) attacks.

The Unified Security Operations Platform

Screenshot of the Unified SOC portal

As of March 2025, Microsoft has introduced the Unified Security Operations Platform (Unified SOC), which brings together Microsoft Sentinel, Microsoft Defender XDR, and Microsoft Security Copilot to create a cohesive security operations experience. The Unified SOC provides:

  • A streamlined incident management experience with a unified incidents queue
  • Enhanced visibility across the security landscape
  • Integrated response capabilities
  • AI-assisted investigation through Security Copilot integration
  • Simplified navigation between SIEM and XDR capabilities

This integration enhances the detection and response capabilities we'll discuss throughout this article.

Unified SOC Incident

Microsoft Sentinel Overview

Microsoft Sentinel remains the foundation of advanced detection and serves as the cloud-native security information and event management (SIEM) and security orchestration, automation, and response (SOAR) solution within the Unified SOC. It provides a single solution for alert detection, threat visibility, proactive hunting, and threat response.

SIEM Capabilities

As a SIEM, Microsoft Sentinel excels at collecting and normalising security data from diverse sources, which is essential for building a comprehensive picture of potential phishing threats. It correlates events across different systems and services, allowing security teams to see connections that might otherwise remain hidden. Through its powerful analytics engine, Sentinel applies detection rules to identify suspicious patterns that could indicate phishing attacks in progress. The platform provides rich visualisation tools for security monitoring that help analysts quickly understand complex security situations. Moreover, it enables proactive threat hunting across collected data, allowing security teams to search for indicators of compromise before alerts are triggered.

SOAR Capabilities for Phishing Response

Sentinel's SOAR functionality delivers orchestrated incident response workflows that guide security teams through consistent remediation processes. When threats are detected, automated actions can be triggered based on predefined criteria, speeding up initial response and containment. The platform includes robust case management for security incidents, ensuring all relevant information is gathered in one place. Integration with ticketing and notification systems keeps relevant stakeholders informed throughout the incident lifecycle. Perhaps most importantly, Sentinel supports playbooks for consistent and efficient response, ensuring that even complex phishing scenarios are handled according to best practices.

Critical Data Sources for Effective Phishing Detection

Effective phishing detection requires visibility across multiple data sources in your digital environment. Modern security teams need integrated monitoring capabilities to identify and respond to these sophisticated threats before they cause significant damage.

Identity Monitoring

Microsoft Entra ID serves as the cornerstone of identity security, providing essential visibility into authentication activities. Sign-in logs reveal unusual patterns from unfamiliar locations or devices that often indicate credential theft following successful phishing. Audit logs complement this by tracking security-relevant changes to user accounts and permissions, potentially revealing lateral movement tactics. As attackers increasingly leverage OAuth techniques, Graph activity logs have become indispensable for detecting token abuse through abnormal API access patterns.

Email Security

Email remains the primary vector for phishing attacks, making comprehensive monitoring essential for early detection. Microsoft Defender for Office 365 captures detailed information about attempted phishing, enabling security teams to understand attack patterns. The platform's URL tracking documents user interactions with suspicious links, often revealing the initial compromise point, while attachment analysis provides critical insights into potential malware behavior attempting to enter your environment.

Endpoint Telemetry

Visibility extends beyond initial access points to endpoint activities, where Microsoft Defender monitors for post-compromise behaviors. Process execution telemetry reveals malware activation following successful phishing, while network connection data exposes command and control communications established by threat actors. File system and browser activity tracking completes the picture, connecting email-based lures to subsequent endpoint compromises.

Unified Security Monitoring

The true power emerges when these data sources converge in the Microsoft Unified Portal, enabling cross-source correlation that reveals complete attack sequences. This integration supports advanced analytics that detect patterns invisible in isolated data and facilitates comprehensive threat hunting across the digital estate. Security teams gain visibility from initial phishing attempt through compromise and subsequent attacker activities, dramatically improving detection and response capabilities.

By implementing this holistic monitoring approach focused on identity systems, email communications, and endpoint behaviours, organisations position themselves to effectively counter the persistent threat of phishing attacks in today's complex security landscape.

Optimising Authentication Data Analysis with UnifiedSignInLogs

When crafting effective detection rules, security professionals often face the challenge of handling disparate authentication log formats. To overcome this obstacle, I've implemented a custom function called UnifiedSignInLogs that seamlessly integrates both interactive and non-interactive authentication events into a cohesive data stream. This approach, originally conceptualised in Fabian Bader's blog post a couple of years ago, has proven invaluable in my workflow. Over time, I've expanded upon Fabian's foundation, enhancing the function with additional capabilities to meet evolving security needs.

union isfuzzy=true SigninLogs, AADNonInteractiveUserSignInLogs
// Rename all columns named _dynamic to normalize the column names
| extend ConditionalAccessPolicies = iff(isempty( ConditionalAccessPolicies_dynamic ), todynamic(ConditionalAccessPolicies_string), ConditionalAccessPolicies_dynamic)
| extend Status = iff(isempty( Status_dynamic ), todynamic(Status_string), Status_dynamic)
| extend MfaDetail = iff(isempty( MfaDetail_dynamic ), todynamic(MfaDetail_string), MfaDetail_dynamic)
| extend DeviceDetail = iff(isempty( DeviceDetail_dynamic ), todynamic(DeviceDetail_string), DeviceDetail_dynamic)
| extend LocationDetails = iff(isempty( LocationDetails_dynamic ), todynamic(LocationDetails_string), LocationDetails_dynamic)
| extend TokenProtection = iff(isempty(TokenProtectionStatusDetails_dynamic),todynamic(TokenProtectionStatusDetails_string),TokenProtectionStatusDetails_dynamic)
// Remove duplicated columns
| project-away *_dynamic, *_string

UnifiedSignInLogs function

This function unifies both interactive (SigninLogs) and non-interactive (AADNonInteractiveUserSignInLogs) authentication events into a single dataset while normalising field formats. This approach offers several advantages for phishing detection.

Alternatively, organisations could implement the Advanced Security Information Model (ASIM) framework instead of this custom function. ASIM provides comprehensive standardisation across multiple security data sources, including authentication events. For organisations already leveraging ASIM, its normalised schema can achieve similar outcomes through existing ASIM parsers without requiring this specific function. ASIM offers the same benefits of normalised field formats and unified visibility, whilst also providing consistency with other security data types beyond authentication events.

Advanced Detection Rules for Modern Phishing Techniques

Let's explore some KQL queries designed to detect sophisticated phishing techniques, including OAuth abuse, token theft, and privilege escalation attempts.

Enhanced Detection: Cross-Origin Device Code Flow Authentication

Using our UnifiedSignInLogs function, we can detect cross-origin device code flow authentication anomalies, which are a telltale sign of AiTM phishing attacks:

UnifiedSignInLogs
| where ClientAppUsed == "Browser"
| project BrowserSignInTime = TimeGenerated
    , UserId, UserPrincipalName, BrowserIPAddress = IPAddress
    , BrowserCity = tostring(LocationDetails.city)
    , BrowserCountry = tostring(LocationDetails.countryOrRegion)
    , BrowserLocation = strcat(tostring(LocationDetails.countryOrRegion)
    , ", ", tostring(LocationDetails.city)), BrowserUserAgent = UserAgent
| join kind=inner (
    UnifiedSignInLogs
    | where AuthenticationProtocol == "deviceCode"
    | project DeviceCodeSignInTime = TimeGenerated, UserId, UserPrincipalName
        , DeviceDetail, DeviceIPAddress = IPAddress, AppDisplayName
        , DeviceCity = tostring(LocationDetails.city)
        , DeviceCountry = tostring(LocationDetails.countryOrRegion)
        , DeviceLocation = strcat(tostring(LocationDetails.countryOrRegion)
        , ", ", tostring(LocationDetails.city))
        , DeviceUserAgent = tostring(UserAgent)
        , ResultType
    ) on UserId
| extend TimeDiffMinutes = datetime_diff('minute', DeviceCodeSignInTime, BrowserSignInTime)
// Look for device code sign-ins within 60 minutes after a browser sign-in
| where TimeDiffMinutes between (0 .. 20) 
| project 
    UserId, UserPrincipalName, BrowserSignInTime, BrowserIPAddress, BrowserLocation
    , BrowserUserAgent, DeviceCodeSignInTime, DeviceIPAddress, DeviceLocation, DeviceUserAgent
    , TimeDiffMinutes, DeviceDetail, AppDisplayName, ResultType
| extend IPMatch = iff(BrowserIPAddress == DeviceIPAddress
    , "Same", "Different")
    , LocationMatch = iff(tostring(BrowserLocation) == tostring(DeviceLocation)
    , "Same", "Different")
    , UserAgentMatch = iff(tostring(BrowserUserAgent) == tostring(DeviceUserAgent)
    , "Same", "Different")
| where IPMatch == "Different" 
    and LocationMatch == "Different" 
    and UserAgentMatch == "Different"
| order by TimeDiffMinutes asc

Cross-Origin Device Code Flow detection

This detection identifies potential Adversary-in-the-Middle (AiTM) attacks by looking for suspicious patterns where:

  1. A user authenticates through a browser
  2. Shortly afterwards (within 20 minutes), a device code authentication occurs
  3. The device code authentication comes from a different IP address, location, and user agent

This pattern is a strong indicator of credential or token theft following a successful phishing attack, particularly when the attacker is using the device code flow to authenticate to applications without needing the second factor.

Block authentication flows with Conditional Access policy - Microsoft Entra ID
Use Conditional Access policy to restrict how device code flow and authentication transfer are used within your organization.

Detecting OAuth Application Abuse

OAuth application abuse is a common technique used in modern phishing attacks to maintain persistent access to victim environments:

// Get most recent identity info to join later, including blast radius indicators
let RecentIdentityInfo = IdentityInfo
    | where TimeGenerated > ago(10d)
    | extend 
        // Parse assigned roles from JSON format
        ParsedRoles = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]"
            , parse_json(AssignedRoles)
            , dynamic([]))
        // Parse group memberships from JSON format
        , ParsedGroups = iff(isnotempty(GroupMembership) 
            and GroupMembership != "[]"
            , parse_json(GroupMembership)
            , dynamic([]))
        // Check for privileged roles
        , IsAdmin = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]"
            , true, false),
        IsPrivilegedRole = iff(
            AssignedRoles has_any("Global Administrator"
                , "Privileged Role Administrator", "User Administrator"
                , "SharePoint Administrator", "Exchange Administrator"
                , "Hybrid Identity Administrator", "Application Administrator"
                , "Cloud Application Administrator")
                , true, false
        ),
        // Check for privileged group memberships
        IsInPrivilegedGroup = iff(
            GroupMembership has_any("AdminAgents"
            , "Azure AD Joined Device Local Administrators"
            , "Directory Synchronization Accounts"
            , "Domain Admins", "Enterprise Admins"
            , "Schema Admins", "Key Admins")
            , true, false
        ),
        Department = Department
        , JobTitle = JobTitle
        , Manager = Manager
    | summarize arg_max(TimeGenerated, *) by AccountObjectId;
// Find specific Graph API calls related to app registration
let appRegistrationEvents = MicrosoftGraphActivityLogs
| where UserAgent has "PowerShell"
| where (RequestUri has_all("https://graph.microsoft.com/v1.0/applications/", "addPassword") 
    or RequestUri == "https://graph.microsoft.com/v1.0/applications" 
    or RequestUri == "https://graph.microsoft.com/v1.0/servicePrincipals")
| extend 
    ApplicationId = tostring(extract(@"applications/(.*?)/addPassword", 1, RequestUri))
    , OperationType = case(
        RequestUri has "addPassword", "Add Credentials"
        , RequestUri == "https://graph.microsoft.com/v1.0/applications"
        , "Create Application"
        , RequestUri == "https://graph.microsoft.com/v1.0/servicePrincipals"
        , "Create Service Principal"
        , "Other"
    );
// Get AAD Audit logs for additional app registration details
let auditLogs = AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName in ("Add application", "Update application — Certificates and secrets management"
    , "Update application")
| extend 
    AppId = tostring(TargetResources[0].id)
    , AppDisplayName = tostring(TargetResources[0].displayName)
    , ModifiedProperties = TargetResources[0].modifiedProperties
    ;
// Join with authentication logs
let appRegistrationWithAuth = appRegistrationEvents
| join kind=leftouter AADNonInteractiveUserSignInLogs
    on $left.SignInActivityId == $right.UniqueTokenIdentifier
| join kind=leftouter RecentIdentityInfo
    on $left.UserId == $right.AccountObjectId;
// Identify users performing multiple app registration operations
appRegistrationWithAuth
| summarize 
    OperationCount = count(),
    OperationTypes = make_set(OperationType),
    FirstOperation = min(TimeGenerated),
    LastOperation = max(TimeGenerated),
    ApplicationIds = make_set(ApplicationId, 10),
    RequestURIs = make_set(RequestUri, 10)
    by 
    UserId, UserPrincipalName, IPAddress, UserAgent, Department, JobTitle
        , Manager, IsAdmin, IsPrivilegedRole, IsInPrivilegedGroup
| extend 
    OperationTimeSpan = datetime_diff('minute', LastOperation, FirstOperation),
    HasAllOperationTypes = array_length(
        set_intersect(OperationTypes, dynamic(
            ["Create Application", "Add Credentials", "Create Service Principal"]
            )
        )) == 3,
    BlastRadiusSeverity = case(
        IsPrivilegedRole == true, "Critical",
        IsAdmin == true 
            or IsInPrivilegedGroup == true, "High"
            , "Medium"
        )
// Focus on patterns indicating Invoke-InjectOAuthApp usage
| where OperationCount >= 3 or HasAllOperationTypes
| project-reorder 
    BlastRadiusSeverity, UserId, UserPrincipalName, IsAdmin, IsPrivilegedRole
        , Department, OperationCount, OperationTypes, HasAllOperationTypes
        , OperationTimeSpan, ApplicationIds, IPAddress, FirstOperation, LastOperation
| order by HasAllOperationTypes desc, BlastRadiusSeverity asc, OperationCount desc

OAuth Application Abuse

This query detects patterns that could indicate malicious OAuth application creation and is particularly effective at identifying activity associated with tools like Invoke-InjectOAuthApp used in AiTM (Adversary-in-the-Middle) phishing campaigns.

Detecting Application and Service Principal Reconnaissance

Before attackers can abuse OAuth, they often perform reconnaissance to identify valuable targets:

let InvokeDumpAppsCalls = dynamic([
    "https://graph.microsoft.com/v1.0/users/",
    "https://graph.microsoft.com/v1.0/organization",
    "https://graph.microsoft.com/v1.0/applications",
    "https://graph.microsoft.com/v1.0/servicePrincipals/",
    'https://graph.microsoft.com/v1.0/servicePrincipals?$skiptoken="'
]);
// Get most recent identity info to join later
let RecentIdentityInfo = 
    IdentityInfo
    | where TimeGenerated > ago(10d)
    | extend 
        ParsedRoles = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]"
            , parse_json(AssignedRoles)
            , dynamic([]))
            , ParsedGroups = iff(isnotempty(GroupMembership) 
            and GroupMembership != "[]"
            , parse_json(GroupMembership)
            , dynamic([]))
            , IsAdmin = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]" 
            and AssignedRoles != "[\"\"]"
            , true, false)
            , IsPrivilegedRole = iff(
                AssignedRoles has_any("Global Administrator", "Privileged Role Administrator"
                    , "User Administrator", "SharePoint Administrator", "Exchange Administrator"
                    , "Hybrid Identity Administrator", "Application Administrator"
                    , "Cloud Application Administrator")
                    , true, false
            ),
        IsInPrivilegedGroup = iff(
            GroupMembership has_any("AdminAgents", "Azure AD Joined Device Local Administrators"
                , "Directory Synchronization Accounts", "Domain Admins", "Enterprise Admins"
                , "Schema Admins", "Key Admins")
                , true, false
        )
    | summarize arg_max(TimeGenerated, *) by AccountObjectId;
// Find Graph API calls that could be suspicious reconnaissance
MicrosoftGraphActivityLogs
| where UserAgent has "PowerShell"
| where RequestUri in~ (InvokeDumpAppsCalls) 
    or RequestUri has_all("https://graph.microsoft.com/v1.0/servicePrincipals(appId=", "appRoleAssignedTo")
| join kind=leftouter AADNonInteractiveUserSignInLogs 
    on $left.SignInActivityId == $right.UniqueTokenIdentifier
| join kind=leftouter RecentIdentityInfo
    on $left.UserId == $right.AccountObjectId
| where isnotempty(UserId) // Only include records where we have a valid UserId
| extend 
    RequestedAppId = extract(@"appId='(.*?)'", 1, RequestUri),
    AdminRoleCount = array_length(ParsedRoles),
    GroupCount = array_length(ParsedGroups),
    UserDisplayName = AccountDisplayName
// Add filters to reduce the number of results
| where ResultType == 0 // Only successful sign-ins
| summarize 
    RequestCount = count()
    , FirstActivity = min(TimeGenerated)
    , LastActivity = max(TimeGenerated)
    , RequestURIs = make_set(RequestUri, 10)
    // , Limit to 10 URIs per group UserAgents = make_set(UserAgent, 5)
    by UserId, UserDisplayName, AccountUPN, UserPrincipalName, IPAddress
        , Department, JobTitle, IsAdmin, IsPrivilegedRole, IsInPrivilegedGroup
        , AdminRoleCount, GroupCount, tostring(ParsedRoles), tostring(ParsedGroups)
        , Scopes
// Only include users who made multiple requests
| where RequestCount > 2 
| extend 
    BlastRadiusSeverity = case(
        IsPrivilegedRole == true, "Critical"
        , IsAdmin == true or IsInPrivilegedGroup == true, "High"
        , AdminRoleCount > 0, "Medium"
        , "Low"
    ),
    ActivityDurationMinutes = datetime_diff('minute', LastActivity, FirstActivity)
    , UniqueEndpointsAccessed = array_length(RequestURIs)
| extend Scope = split(Scopes, " ")
| extend ScopeCount = array_length(Scope)
| project-away Scopes
| order by BlastRadiusSeverity asc, RequestCount desc, ActivityDurationMinutes desc

Application and Service Principal Reconnaissance

This detection can identify attacker reconnaissance activities using tools like Invoke-DumpApps and similar PowerShell modules that enumerate Azure AD applications and service principals.

Detecting Security Group Cloning and Manipulation

Attackers may attempt to clone security groups to gain elevated permissions:

// Get most recent identity info to join later, including blast radius indicators
let RecentIdentityInfo = IdentityInfo
    | where TimeGenerated > ago(10d)
    | extend 
        // Parse assigned roles from JSON format
        ParsedRoles = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]", parse_json(AssignedRoles), dynamic([]))
        // Parse group memberships from JSON format
        , ParsedGroups = iff(isnotempty(GroupMembership) 
            and GroupMembership != "[]", parse_json(GroupMembership), dynamic([]))
        // Check for privileged roles
        , IsAdmin = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]", true, false)
        , IsPrivilegedRole = iff(
            AssignedRoles has_any("Global Administrator", "Privileged Role Administrator"
                , "User Administrator", "SharePoint Administrator", "Exchange Administrator"
                , "Hybrid Identity Administrator", "Application Administrator"
                , "Cloud Application Administrator")
                , true, false
        ),
        // Check for privileged group memberships
        IsInPrivilegedGroup = iff(
            GroupMembership has_any("AdminAgents", "Azure AD Joined Device Local Administrators"
                , "Directory Synchronization Accounts", "Domain Admins", "Enterprise Admins"
                , "Schema Admins", "Key Admins")
                , true, false
        ),
        Department = Department
        , JobTitle = JobTitle
        , Manager = Manager
    | summarize arg_max(TimeGenerated, *) by AccountObjectId;
// Detect Graph Activity related to security groups cloning
let groupModificationEvents = MicrosoftGraphActivityLogs
| where UserAgent has "PowerShell"
| where RequestUri has_all("https://graph.microsoft.com/v1.0/groups/", "/members/$ref")
    or RequestUri has_all("https://graph.microsoft.com/v1.0/groups", "/members")
    or RequestUri == "https://graph.microsoft.com/v1.0/groups?=securityEnabled%20eq%20true"
    or RequestUri == "https://graph.microsoft.com/v1.0/me"
| extend 
    GroupObjectId = tostring(extract(@"groups/(.*?)/members", 1, RequestUri)),
    OperationType = case(
        RequestUri has "/members/$ref", "Add Member",
        RequestUri has "/members" 
            and not(RequestUri has "/members/$ref"), "List Members",
        RequestUri has "securityEnabled%20eq%20true", "List Security Groups",
        RequestUri has "/me", "Get Current User",
        "Other"
    );
// Join with authentication logs and identity info
let groupEventsWithContext = groupModificationEvents
| join kind=leftouter AADNonInteractiveUserSignInLogs
    on $left.SignInActivityId == $right.UniqueTokenIdentifier
| join kind=leftouter RecentIdentityInfo
    on $left.UserId == $right.AccountObjectId;
// Get group creation events from audit logs to correlate with group member additions
let groupCreationEvents = AuditLogs
| where TimeGenerated > ago(1d)
| where OperationName == "Add group" 
| where Result == "success"
| extend ActorId = tostring(InitiatedBy.user.id)
| project 
    TimeGenerated, ActorId, GroupId = tostring(TargetResources[0].id)
        , GroupName = tostring(TargetResources[0].displayName);
// Identify patterns of security group cloning
groupEventsWithContext
| summarize 
    OperationCount = count()
        , OperationTypes = make_set(OperationType)
        , SecurityGroupsAccessed = make_set(GroupObjectId, 15)
        , FirstOperation = min(TimeGenerated)
        , LastOperation = max(TimeGenerated)
        , RequestURIs = make_set(RequestUri, 10)
    by 
    UserId, UserPrincipalName, IPAddress, UserAgent, Department
        , JobTitle, Manager, IsAdmin, IsPrivilegedRole, IsInPrivilegedGroup
| extend 
    OperationTimeSpan = datetime_diff('minute', LastOperation, FirstOperation)
    , HasListAndModifyOperations = array_length(
        set_intersect(OperationTypes, dynamic(["List Security Groups", "List Members", "Add Member"])
        )) >= 2,
    SecurityGroupCount = array_length(
        SecurityGroupsAccessed)
            , BlastRadiusSeverity = case(
                IsPrivilegedRole == true, "Critical"
                , IsAdmin == true or IsInPrivilegedGroup == true, "High"
                , "Medium"
            )
// Focus on patterns indicating security group cloning
| where OperationCount >= 4 
    and HasListAndModifyOperations
| project-reorder 
    BlastRadiusSeverity, UserId, UserPrincipalName, IsAdmin, IsPrivilegedRole, Department, OperationCount
        , OperationTypes, HasListAndModifyOperations, SecurityGroupCount, OperationTimeSpan
        , SecurityGroupsAccessed, IPAddress, FirstOperation, LastOperation
| order by BlastRadiusSeverity asc, OperationCount desc

Security Group Cloning

Detecting Privileged Role Management Activities

Privilege escalation often involves manipulating role management capabilities:

// Get most recent identity info to join later, including blast radius indicators
let RecentIdentityInfo = IdentityInfo
    | where TimeGenerated > ago(10d)
    | extend 
        // Parse assigned roles from JSON format
        ParsedRoles = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]"
            , parse_json(AssignedRoles)
            , dynamic([]))
        // Parse group memberships from JSON format
        , ParsedGroups = iff(isnotempty(GroupMembership) 
            and GroupMembership != "[]"
            , parse_json(GroupMembership)
            , dynamic([]))
        // Check for privileged roles
        , IsAdmin = iff(isnotempty(AssignedRoles) 
            and AssignedRoles != "[]"
            , true, false)
        , IsPrivilegedRole = iff(
            AssignedRoles has_any("Global Administrator", "Privileged Role Administrator"
                , "User Administrator", "SharePoint Administrator", "Exchange Administrator"
                , "Hybrid Identity Administrator", "Application Administrator"
                , "Cloud Application Administrator")
                , true, false
        ),
        // Check for privileged group memberships
        IsInPrivilegedGroup = iff(
            GroupMembership has_any("AdminAgents", "Azure AD Joined Device Local Administrators"
                , "Directory Synchronization Accounts", "Domain Admins", "Enterprise Admins"
                , "Schema Admins", "Key Admins")
                , true, false
        )
        , EmployeeId = JobTitle
        , Department = Department
        , Manager = Manager
    // Take only the most recent record per account
    | summarize arg_max(TimeGenerated, *) by AccountObjectId; 
// Find Graph API calls accessing role management or generic groups
MicrosoftGraphActivityLogs
| where UserAgent has "PowerShell"
| where RequestUri == "https://graph.microsoft.com/beta/roleManagement/directory/estimateAccess" 
    or RequestUri == "https://graph.microsoft.com/v1.0/groups"
| join kind = leftouter AADNonInteractiveUserSignInLogs
    on $left.SignInActivityId == $right.UniqueTokenIdentifier
| join kind = leftouter RecentIdentityInfo
    on $left.UserId == $right.AccountObjectId
| where isnotempty(UserId) // Only include records where we have a valid UserId
| extend 
    UserDisplayName = iff(isnotempty(AccountDisplayName)
    , AccountDisplayName, UserDisplayName)
    , RoleCount = iff(isnotempty(ParsedRoles)
    , array_length(ParsedRoles), 0)
    , GroupCount = iff(isnotempty(ParsedGroups)
    , array_length(ParsedGroups), 0)
    , KeyAdminGroups = iff(isnotempty(ParsedGroups)
    , set_intersect(ParsedGroups, dynamic(["AdminAgents", "Azure AD Joined Device Local Administrators"
        , "Directory Synchronization Accounts", "Domain Admins", "Enterprise Admins"
        , "Schema Admins", "Key Admins", "Azure DevOps Administrators", "Security Administrators"
        , "Global Readers"]))
        , dynamic([]))
        , AccessType = case(
        RequestUri == "https://graph.microsoft.com/beta/roleManagement/directory/estimateAccess"
            , "Role Management Access Estimation",
        RequestUri == "https://graph.microsoft.com/v1.0/groups"
            , "All Groups Enumeration", "Other Access"
        )
// Add filters to reduce the number of results
| where ResultType == 0 or isnull(ResultType) // Only successful sign-ins or when ResultType isn't available
| summarize 
    RequestCount = count()
    , FirstActivity = min(TimeGenerated)
    , LastActivity = max(TimeGenerated)
    , RequestURIs = make_set(RequestUri, 10)
    , UserAgents = make_set(UserAgent, 5)
    , AccessTypes = make_set(AccessType)
    by 
    UserId, UserDisplayName,AccountUPN, UserPrincipalName, IPAddress,Department,EmployeeId
        , Manager,IsAdmin,IsPrivilegedRole,IsInPrivilegedGroup,tostring(ParsedRoles)
        ,RoleCount,tostring(KeyAdminGroups),GroupCount
| extend 
    BlastRadiusSeverity = case(
        IsPrivilegedRole == true, "Critical",
        IsAdmin == true 
            or IsInPrivilegedGroup == true, "High",
        RoleCount > 0, "Medium",
        "Low"
    ),
    ActivityDurationMinutes = datetime_diff('minute', LastActivity, FirstActivity)
    , UniqueEndpointsAccessed = array_length(RequestURIs)
| order by BlastRadiusSeverity asc, RequestCount desc, ActivityDurationMinutes desc

Privileged Role Management Activities

fThis detection helps identify users who are performing reconnaissance on role management capabilities or enumerating all groups, which could be precursors to privilege escalation attempts. It's particularly effective at identifying attackers who may have compromised a standard user account through phishing and are now looking to escalate privileges.

Implementing Detection Rules in Unified SOC

With our detection queries defined, we now need to implement them as analytics rules in Microsoft Sentinel.

Creating Analytics Rules from KQL

To implement our detection queries in Sentinel:

  1. Navigate to Analytics Rules: In the Sentinel section of the Unified SOC portal, navigate to Configuration > Analytics
  2. Create New Rule: Choose "Scheduled query rule" to create a new detection
  3. Define Query Logic: Paste and adapt the KQL queries shown earlier in this article
  4. Set Parameters: Configure appropriate query frequency, lookup window, and threshold for your environment
  5. Define Alerts: Set alert severity based on your organisation's risk assessment
  6. Configure Entities: Map to appropriate entity types (users, hosts, URLs) for investigation
  7. Create Incident Settings: Determine how alerts should group into incidents
  8. Set Automated Responses: Connect to playbooks for automated remediation
Analytics Rule creation in Microsoft Sentinel
All the rules and subsequent hunting queries that I have built for this blog series will be released in Yaml and ARM following my session in Las Vegas.

Alert Tuning Considerations

While implementing these detections, consider these tuning approaches to reduce false positives:

  1. Baseline normal user behaviour: Establish normal patterns before setting thresholds
  2. Whitelist admin activity: Add exclusions for legitimate administrative activity
  3. Consider time windows: Adjust detection timeframes to match your organisation's working patterns
  4. Implement risk scoring: Use a combination of indicators rather than single triggers
  5. Leverage entity analytics: Use Sentinel's entity behaviour analytics to identify anomalies

Automated Response Playbooks for Modern Phishing Attacks

The Unified SOC platform enhances Sentinel's playbook capabilities for automated response. Here are key playbooks for addressing modern phishing techniques:

Breach Defence Automation: Creating Your Hybrid Account Kill Switch with Microsoft Sentinel and Logic Apps
In cybersecurity, response time is everything. When a suspicious user account is detected, every second counts. Today, I’m sharing a comprehensive security automation tool I’ve built using Logic Apps to respond to Microsoft Sentinel incidents by automatically disabling compromised accounts across both Entra ID and on-premises Active Directory environments.
GitHub - briandelmsft/SentinelAutomationModules: The Microsoft Sentinel Triage AssistanT (STAT) enables easy to create incident triage automation in Microsoft Sentinel
The Microsoft Sentinel Triage AssistanT (STAT) enables easy to create incident triage automation in Microsoft Sentinel - briandelmsft/SentinelAutomationModules
Building an Automated Sentinel Incident Reporting System with Azure Logic Apps
Sentinel Alerts to Actionable Insights: Streamlining Security Incident Communication with Power-Packed Logic Apps
You can also leverage the built in functionality in the Unified SOC

Unified SOC Investigation Workflow

The Unified SOC platform enhances the investigation and response process by bringing together Sentinel's detection capabilities with Defender XDR's endpoint protection and Security Copilot's AI-assisted investigation.

The Unified Investigation Experience

When investigating a phishing incident in the Unified SOC:

  1. Centralised Incident View: Access all phishing incidents from the unified incidents queue
  2. Cross-Platform Investigation: Seamlessly move between Sentinel and Defender XDR views
  3. AI-Assisted Analysis: Use Security Copilot to help interpret complex authentication patterns
  4. Timeline Correlation: View the attack timeline across email, identity, and endpoint events
  5. Evidence Collection: Gather evidence from all affected systems in a single interface
  6. Automated Enrichment: Leverage threat intelligence to understand attack context
  7. Guided Response: Follow recommended response actions based on the attack type
incident investigation in Unified SOC

Conclusion: Vigilance Beyond Prevention

While preventative controls are essential, the sophisticated nature of modern phishing attacks makes robust detection capabilities equally important. By implementing comprehensive monitoring with Microsoft Sentinel and the Unified SOC, organisations can identify successful phishing attempts quickly and respond effectively to limit their impact.

In the final instalment of our series, Part 6, we'll explore how to implement conditional access strategies alongside phishing-resistant authentication methods like passkeys and FIDO2 security keys. These technologies provide the strongest defence against even the most sophisticated phishing techniques we've examined throughout this series.


This article is part of a six-part series (plus bonus content) on phishing attacks and defences. Read Part 1: Introduction to the Blog Series, Part 2: Understanding Modern Phishing Attacks, Part 3: Inside the Attacker's Toolkit, our bonus content on token theft, and Part 4: Comprehensive Phishing Defences if you haven't already, and stay tuned for the final instalment leading to my presentation at the Microsoft 365 Community Conference in Las Vegas on 6-8 May 2025.


CTA Image

I hope you've found this guide helpful in enhancing your security posture. If you've enjoyed this content and would like to support more like it, please consider joining the Supporters Tier. Your support helps me continue creating practical security automation content for the community.

Supporters Tier Upgrade