Adversaries may use automated scripts to rapidly assign directory roles within a short timeframe to establish persistent access and escalate privileges. SOC teams should proactively hunt for this behavior in Azure Sentinel to detect potential post-compromise activity and mitigate lateral movement risks.
KQL Query
let timeframe = 1d;
let window = 10m;
let threshold = 3;
// Role assignment events expanded to extract the target user per event
let RoleAssignments =
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName =~ "Add member to role."
| where Result =~ "success"
| extend ActorUpn = tolower(tostring(InitiatedBy.user.userPrincipalName))
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend ActorIp = iff(
isnotempty(tostring(InitiatedBy.user.ipAddress)),
tostring(InitiatedBy.user.ipAddress),
tostring(InitiatedBy.app.ipAddress))
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| mv-expand TargetResource = TargetResources
| where tostring(TargetResource.type) =~ "User"
| extend TargetUpn = tostring(TargetResource.userPrincipalName)
| where isnotempty(TargetUpn);
// Aggregate by actor and fixed ten-minute bucket, flag where count reaches threshold
// Note: bin() uses fixed time buckets; assignments at bucket boundaries may be undercounted.
// For sliding-window accuracy, consider using a range join approach.
let BulkActors =
RoleAssignments
| summarize
FirstAssignment = min(TimeGenerated),
LastAssignment = max(TimeGenerated),
AssignedUsers = make_set(TargetUpn),
AssignmentCount = count(),
ActorIp = any(ActorIp)
by Actor, bin(TimeGenerated, window)
| where AssignmentCount >= threshold
| extend WindowDurationSeconds = datetime_diff('second', LastAssignment, FirstAssignment);
// Enrich with actor most recent sign-in country for geographic context
let ActorSignIns =
SigninLogs
| where TimeGenerated >= ago(timeframe)
| where ResultType == 0
| extend ActorUpn = tolower(UserPrincipalName)
| summarize LastSignInTime = max(TimeGenerated) by ActorUpn, Location
| summarize arg_max(LastSignInTime, Location) by ActorUpn
| project ActorUpn, LastSignInCountry = Location;
BulkActors
| join kind=leftouter ActorSignIns on $left.Actor == $right.ActorUpn
| extend AccountName = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
| project
FirstAssignment,
LastAssignment,
WindowDurationSeconds,
Actor,
AccountName,
AccountUPNSuffix,
ActorIp,
AssignmentCount,
AssignedUsers,
LastSignInCountry
| sort by FirstAssignment desc
id: 8d2cc40f-f0e0-49bf-8983-164f7be3975d
name: Bulk role assignments performed by the same actor in a short window
description: |
Identifies actors who perform three or more Entra ID directory role assignments
within a ten-minute window, consistent with automated post-compromise persistence.
Results are enriched with the actor's most recent sign-in country for analyst triage.
Adjust the threshold variable for environments with routine bulk provisioning workflows.
References:
- https://learn.microsoft.com/azure/active-directory/roles/permissions-reference
- https://learn.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities
- https://attack.mitre.org/techniques/T1098/003/
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
- SigninLogs
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098.003
query: |
let timeframe = 1d;
let window = 10m;
let threshold = 3;
// Role assignment events expanded to extract the target user per event
let RoleAssignments =
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName =~ "Add member to role."
| where Result =~ "success"
| extend ActorUpn = tolower(tostring(InitiatedBy.user.userPrincipalName))
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend ActorIp = iff(
isnotempty(tostring(InitiatedBy.user.ipAddress)),
tostring(InitiatedBy.user.ipAddress),
tostring(InitiatedBy.app.ipAddress))
| extend Actor = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| mv-expand TargetResource = TargetResources
| where tostring(TargetResource.type) =~ "User"
| extend TargetUpn = tostring(TargetResource.userPrincipalName)
| where isnotempty(TargetUpn);
// Aggregate by actor and fixed ten-minute bucket, flag where count reaches threshold
// Note: bin() uses fixed time buckets; assignments at bucket boundaries may be undercounted.
// For sliding-window accuracy, consider using a range join approach.
let BulkActors =
RoleAssignments
| summarize
FirstAssignment = min(TimeGenerated),
LastAssignment = max(TimeGenerated),
AssignedUsers = make_set(TargetUpn),
AssignmentCount = count(),
ActorIp = any(ActorIp)
by Actor, bin(TimeGenerated, window)
| where AssignmentCount >= threshold
| extend WindowDurationSeconds = datetime_diff('second', LastAssignment, FirstAssignment);
// Enrich with actor most recent sign-in country for geographic context
let ActorSignIns =
SigninLogs
| where TimeGenerated >= ago(timeframe)
| where ResultType == 0
| extend ActorUpn = tolower(UserPrincipalName)
| summarize LastSignInTime = max(TimeGenerated) by ActorUpn, Location
| summarize arg_max(LastSignInTime, Location) by ActorUpn
| project ActorUpn, LastSig
| Sentinel Table | Notes |
|---|---|
AuditLogs | Ensure this data connector is enabled |
SigninLogs | Ensure this data connector is enabled |
Scenario: Scheduled Job Performing Role Assignments
Description: A system-managed service account runs a scheduled job that assigns roles to multiple users as part of an automated provisioning process.
Filter/Exclusion: Exclude activities originating from known service accounts or scheduled tasks (e.g., Azure Automation, PowerShell DSC, or Azure DevOps pipelines) using the actor_id or source field.
Scenario: Role Assignment via PowerShell Script
Description: An admin runs a PowerShell script to assign roles to multiple users during a bulk onboarding process.
Filter/Exclusion: Exclude activities where the actor is a known admin user and the source is a script or command-line interface (e.g., PowerShell, Azure CLI). Use the source or client_ip field to identify script-based activity.
Scenario: Role Assignment During User Migration
Description: A migration tool (e.g., Azure Migrate, Azure AD Connect) assigns roles to users during a bulk migration process.
Filter/Exclusion: Exclude activities where the actor is a migration tool or service (e.g., Azure Migrate, Azure AD Connect) and the operation is related to migration or synchronization.
Scenario: Role Assignment via Azure AD Privileged Identity Management (PIM)
Description: An admin uses Azure AD PIM to assign roles to multiple users as part of a temporary access request.
Filter/Exclusion: Exclude activities where the actor is associated with PIM and the operation is related to role activation or access review. Use the operation_name or actor_type field to identify PIM-related activity.
Scenario: Role Assignment During System Health Check
Description: A system health check or compliance tool (e.g., Microsoft Intune