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

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.

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:
- A user authenticates through a browser
- Shortly afterwards (within 20 minutes), a device code authentication occurs
- 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.

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:
- Navigate to Analytics Rules: In the Sentinel section of the Unified SOC portal, navigate to Configuration > Analytics
- Create New Rule: Choose "Scheduled query rule" to create a new detection
- Define Query Logic: Paste and adapt the KQL queries shown earlier in this article
- Set Parameters: Configure appropriate query frequency, lookup window, and threshold for your environment
- Define Alerts: Set alert severity based on your organisation's risk assessment
- Configure Entities: Map to appropriate entity types (users, hosts, URLs) for investigation
- Create Incident Settings: Determine how alerts should group into incidents
- Set Automated Responses: Connect to playbooks for automated remediation

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:
- Baseline normal user behaviour: Establish normal patterns before setting thresholds
- Whitelist admin activity: Add exclusions for legitimate administrative activity
- Consider time windows: Adjust detection timeframes to match your organisation's working patterns
- Implement risk scoring: Use a combination of indicators rather than single triggers
- 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:



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

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.
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.