Detection rules › Kusto
[Entra ID] Privileged Role Assigned to a New User
Detects when a privileged role is assigned to a new user account. This can indicate unauthorized elevation of privileges.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | |
| Persistence | |
| Privilege Escalation | |
| Stealth |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | any: Entra ID audit event (any operation) |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Mass Privileged Role Change Activity Detected (Kusto)
- [Entra ID] Privilege Elevation Request Denied (Kusto)
- [Entra ID] Privileged Role Assigned to User (Kusto)
- Account Elevated to New Role (Kusto)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Assigned To Azure RBAC/Microsoft Entra Role (Sigma)
- Azure AD Application Administrator Role Assigned (Splunk)
Rule body
id: b64ac0e2-a241-4b9a-ad3e-ae572630b295
name: "[Entra ID] Privileged Role Assigned to a New User"
version: 1.0.0
kind: Scheduled
description: |
Detects when a privileged role is assigned to a new user account. This can indicate unauthorized elevation of privileges.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
- DefenseEvasion
- InitialAccess
relevantTechniques:
- T1078.004
query: |
// Define the start and end times based on input values
let starttime = now() - 1h;
let endtime = now();
// Set a lookback period of 14 days
let lookback = starttime - 14d;
// Define a reusable function to query audit logs
let awsFunc = (start: datetime, end: datetime) {
AuditLogs
| where TimeGenerated between (start .. end)
| where Category =~ "RoleManagement"
| where AADOperationType in ("Assign", "AssignEligibleRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type in~ ("User", "ServicePrincipal")
| extend
Target = iff(TargetResource.type =~ "ServicePrincipal", tostring(TargetResource.displayName), tostring(TargetResource.userPrincipalName)),
props = TargetResource.modifiedProperties
)
| mv-apply Property = props on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"', tostring(Property.newValue))
)
| where RoleName contains "Admin" and Result == "success"
};
// Query for audit events in the current day
let EventInfo_CurrentDay = awsFunc(starttime, endtime);
// Query for audit events in the historical period (lookback)
let EventInfo_historical = awsFunc(lookback, starttime);
// Find unseen events by performing a left anti-join
let EventInfo_Unseen = (EventInfo_CurrentDay
| join kind=leftanti(EventInfo_historical) on Target, RoleName, OperationName
);
// Extend and clean up the results
EventInfo_Unseen
| extend InitiatingAppName = tostring(InitiatedBy.app.displayName)
| extend InitiatingAppServicePrincipalId = tostring(InitiatedBy.app.servicePrincipalId)
| extend InitiatingUserPrincipalName = tostring(InitiatedBy.user.userPrincipalName)
| extend InitiatingAadUserId = tostring(InitiatedBy.user.id)
| extend InitiatingIpAddress = tostring(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
| extend Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
// You can uncomment the lines below to filter out PIM activations
| where Initiator != "MS-PIM"
//| summarize StartTime=min(TimeGenerated), EndTime=min(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result
// Project specific columns and split them for further analysis
| project
TimeGenerated,
OperationName,
RoleName,
Target,
Initiator,
InitiatingUserPrincipalName,
InitiatingAadUserId,
InitiatingAppName,
InitiatingAppServicePrincipalId,
InitiatingIpAddress,
Result
| extend
TargetName = tostring(split(Target, '@', 0)[0]),
TargetUPNSuffix = tostring(split(Target, '@', 1)[0]),
InitiatorName = tostring(split(InitiatingUserPrincipalName, '@', 0)[0]),
InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName, '@', 1)[0])
| extend
Source_Network_IPLocation = "",
ActivityType=OperationName
| project
Alert_Category_en = "Entra ID",
Alert_SubCategory_en = "Anomaly Identity Privilege Modification",
Alert_Name_en = "Privileged Role Assigned to a New User",
Alert_Description_en = strcat(
"At Taiwan time: ",
format_datetime(datetime_utc_to_local(TimeGenerated, "Asia/Taipei"), "yyyy-MM-dd HH:mm:ss"),
"in the Microsoft Entra ID tenant",
"operator: ",
iff(isnotempty(Initiator), Initiator, InitiatingAppName),
", from IP: ",
InitiatingIpAddress,
", assigned privileged role ",
tostring(RoleName),
" to ",
iff(isnotempty(Target), Target, "<NoTargetUser>"),
". This permission change behavior has not appeared for this user recently (within 14 days)."
),
Alert_TriageStep_en = strcat(
"1. Confirm whether the privileged role assignment was performed through an Entra ID PIM-approved change process.",
"2. Confirm with the operator: ",
iff(isnotempty(Initiator), Initiator, "<UnknownInitiator>"),
" whether this was performed by the account owner.",
"3. Check whether the source IP and geolocation are abnormal or not a company public IP."
),
Alert_Containment_en = strcat(
"1. If determined to be an unauthorized role assignment, immediately revoke the target ",
iff(isnotempty(Target), Target, "<NoTargetUser>"),
" 's privileged role (",
tostring(RoleName),
"), and restore the state to before the change.",
"2. Immediately restrict the source identity (user or application) that performed the role assignment, revoke sign-in tokens, and force password and MFA reset if necessary. ",
"3. If the source is an application (",
iff(isnotempty(InitiatingAppName), InitiatingAppName, "<NoServicePrincipal>"),
"), immediately disable the Service Principal and revoke high-privilege API consent and credentials/secrets. "
),
Alert_Remediation_en = strcat(
"1. Allow privileged role access only through Entra ID PIM.",
"2. Enforce Conditional Access and MFA for role management and PIM operations, allowing them only from managed devices and named locations.",
"3. Establish real-time alerts and automated response (SOAR) for privileged role changes to detect abnormal role assignments immediately. "
),
Alert_Time_TW = datetime_utc_to_local(TimeGenerated, "Asia/Taipei"),
Alert_Time_UTC0 = TimeGenerated,
Event_Action = ActivityType,
Event_Status = Result,
//Event_Description = ActivityDisplayName,
Source_Identity_FullName = iff(isnotempty(Initiator), Initiator, InitiatingAppName),
//Source_Identity_ID = ActorID,
Source_Identity_Type = iff(isnotempty(Initiator), "User", "Service"),
Source_Network_IPAddress = InitiatingIpAddress,
Source_Network_IPLocation = Source_Network_IPLocation,
//Source_Resource_Name = InitiatingAppName,
Target_Identity_FullName = Target,
Target_Identity_DomainType = iff(Target contains "EXT", 'External', 'Internal'),
Target_Identity_Type = "User",
Target_Resource_ID = "",
Target_Resource_Type = "Entra ID"
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Source_Identity_FullName
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Target_Identity_FullName
- entityType: IP
fieldMappings:
- identifier: Address
columnName: Source_Network_IPAddress
Stages and Predicates
Parameters
let starttime = now() - 1h;
let endtime = now();
let lookback = starttime - 14d;
let EventInfo_CurrentDay = awsFunc(starttime, endtime);
let EventInfo_historical = awsFunc(lookback, starttime);
let EventInfo_Unseen is inlined into the numbered stages below.
Let binding: awsFunc
let awsFunc = (start: datetime, end: datetime) {
AuditLogs
| where TimeGenerated between (start .. end)
| where Category =~ "RoleManagement"
| where AADOperationType in ("Assign", "AssignEligibleRole")
| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
| mv-apply TargetResource = TargetResources on
(
where TargetResource.type in~ ("User", "ServicePrincipal")
| extend
Target = iff(TargetResource.type =~ "ServicePrincipal", tostring(TargetResource.displayName), tostring(TargetResource.userPrincipalName)),
props = TargetResource.modifiedProperties
)
| mv-apply Property = props on
(
where Property.displayName =~ "Role.DisplayName"
| extend RoleName = trim('"', tostring(Property.newValue))
)
| where RoleName contains "Admin" and Result == "success"
};
Stage 1: source
AuditLogs
Stage 2: where
where TimeGenerated >= "start" and TimeGenerated <= "end"
Stage 3: where
where Category =~ "RoleManagement"
Stage 4: where
where AADOperationType in~ ("Assign", "AssignEligibleRole")
Stage 5: where
where (ActivityDisplayName contains "Add eligible member to role" or ActivityDisplayName contains "Add member to role")
Stage 6: kusto:mv-apply
kusto:mv-apply type in~ ("ServicePrincipal", "User")
Stage 7: kusto:mv-apply
kusto:mv-apply displayName =~ "Role.DisplayName"
Stage 8: where
where Result =~ "success" and RoleName contains "Admin"
Stage 9: join (negated)
join kind=leftanti (EventInfo_historical) on Target, RoleName, OperationName
Stage 10: extend (6 consecutive steps)
extend InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator
Stage 11: where
where Initiator !~ "MS-PIM"
Stage 12: project
project InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator, OperationName, Result, RoleName, Target, TimeGenerated
Stage 13: extend
extend InitiatorName, InitiatorUPNSuffix, TargetName, TargetUPNSuffix
Stage 14: extend
extend ActivityType, Source_Network_IPLocation
Stage 15: project
project Alert_Category_en, Alert_Containment_en, Alert_Description_en, Alert_Name_en, Alert_Remediation_en, Alert_SubCategory_en, Alert_Time_TW, Alert_Time_UTC0, Alert_TriageStep_en, Event_Action, Event_Status, Source_Identity_FullName, Source_Identity_Type, Source_Network_IPAddress, Source_Network_IPLocation, Target_Identity_DomainType, Target_Identity_FullName, Target_Identity_Type, Target_Resource_ID, Target_Resource_Type
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
AADOperationType | in | Assign, AssignEligibleRole | excludes:AADOperationType field:"AADOperationType" value:"Assign" field:"AADOperationType" value:"AssignEligibleRole" |
ActivityDisplayName | match | Add eligible member to role, Add member to role | excludes:ActivityDisplayName field:"ActivityDisplayName" value:"Add eligible member to role" field:"ActivityDisplayName" value:"Add member to role" |
Category | eq | RoleManagement | excludes:Category field:"Category" value:"RoleManagement" |
Result | eq | success | excludes:Result field:"Result" value:"success" |
RoleName | contains | Admin | excludes:RoleName field:"RoleName" value:"Admin" |
TimeGenerated | ge | start | excludes:TimeGenerated field:"TimeGenerated" value:"start" |
TimeGenerated | le | end | excludes:TimeGenerated field:"TimeGenerated" value:"end" |
displayName | eq | Role.DisplayName | excludes:displayName field:"displayName" value:"Role.DisplayName" |
type | in | ServicePrincipal, User | excludes:type field:"type" value:"ServicePrincipal" field:"type" value:"User" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
AADOperationType | in |
| field:"AADOperationType" kind:in |
ActivityDisplayName | match |
| field:"azure_ad::activity_display_name" kind:match |
Category | eq |
| field:"Category" kind:eq value:"RoleManagement" |
Initiator | ne |
| field:"Initiator" kind:ne value:"MS-PIM" |
Result | eq |
| field:"Result" kind:eq value:"success" |
RoleName | contains |
| field:"RoleName" kind:contains value:"Admin" |
TimeGenerated | ge |
| field:"TimeGenerated" kind:ge value:"start" |
TimeGenerated | le |
| field:"TimeGenerated" kind:le value:"end" |
displayName | eq |
| field:"displayName" kind:eq value:"Role.DisplayName" |
type | in |
| field:"type" kind:in |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
Alert_Category_en | project |
Alert_Containment_en | project |
Alert_Description_en | project |
Alert_Name_en | project |
Alert_Remediation_en | project |
Alert_SubCategory_en | project |
Alert_Time_TW | project |
Alert_Time_UTC0 | project |
Alert_TriageStep_en | project |
Event_Action | project |
Event_Status | project |
Source_Identity_FullName | project |
Source_Identity_Type | project |
Source_Network_IPAddress | project |
Source_Network_IPLocation | project |
Target_Identity_DomainType | project |
Target_Identity_FullName | project |
Target_Identity_Type | project |
Target_Resource_ID | project |
Target_Resource_Type | project |