Detection rules › Kusto

[Entra ID] Privileged Role Assigned to User

Status
available
Severity
high
Time window
1h
Group by
InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator, OperationName, Result, RoleName, Target
Source
github.com/Azure/Azure-Sentinel

Detects when a user is assigned a privileged role in Microsoft Entra ID. This may indicate unauthorized privilege escalation and should be validated promptly.

MITRE ATT&CK coverage

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: fad0f8b9-a0a5-430a-9a94-049a1144bf54
name: "[Entra ID] Privileged Role Assigned to User"
version: 1.0.0
kind: Scheduled
description: |
  Detects when a user is assigned a privileged role in Microsoft Entra ID. This may indicate unauthorized privilege escalation and should be validated promptly.
severity: High
status: Available
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
  - DefenseEvasion
  - PrivilegeEscalation
  - InitialAccess
relevantTechniques:
  - T1078.004
query: |
  AuditLogs
  | where Category =~ "RoleManagement"
  | where AADOperationType in ("Assign", "AssignEligibleRole", "CreateRequestGrantedRole", "CreateRequestPermanentEligibleRole", "CreateRequestPermanentGrantedRole")
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
  | mv-apply TargetResourceSubject = TargetResources on
      (
      where TargetResourceSubject.type in~ ("User", "ServicePrincipal")
      | extend
          Target = iff(TargetResourceSubject.type =~ "ServicePrincipal", tostring(TargetResourceSubject.displayName), tostring(TargetResourceSubject.userPrincipalName)),
          subjectProps = TargetResourceSubject.modifiedProperties
      )
  | mv-apply TargetResourceRole = TargetResources on
      (
      // mimic modifiedProperties so we can use the same logic to get the role name regardless of where it comes from
      where TargetResourceRole.type in~ ("Role")
      | extend roleProps = pack_array(bag_pack("displayName", "Role.DisplayName", "newValue", TargetResourceRole.displayName))
      )
  | mv-apply Property = iff(array_length(subjectProps) > 0, subjectProps, roleProps) on
      (
      where Property.displayName =~ "Role.DisplayName"
      | extend RoleName = trim('"', tostring(Property.newValue))
      )
  | where RoleName contains "Admin"
  | 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)
  // Comment below to alert for PIM activations
  | where Initiator != "MS-PIM"
  | summarize
      by
      bin(TimeGenerated, 1h),
      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 = "User Assigned a Privileged Role",
      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, "<UnknownInitiator>"),
                             ", from IP: ",
                             InitiatingIpAddress,
                             ", assigned privileged role  ",
                             tostring(RoleName),
                             "  to user  ",
                             iff(isnotempty(Target), Target, "<NoTargetUser>"),
                             ". This behavior is a high-risk permission change and must be validated immediately."
                         ),
      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 this is determined to be an unauthorized role assignment, immediately revoke the privileged role from target user  ",
                             iff(isnotempty(Target), Target, "<NoTargetUser>"),
                             " 's privileged role (",
                             tostring(RoleName),
                             "), and restore the state to before the change.",
                             "2. Immediately restrict or suspend the operator that performed the role assignment (user or application), revoke sign-in tokens, and force MFA/password reset as needed.  ",
                             "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(InitiatingUserPrincipalName), "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

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "RoleManagement"

Stage 3: where

| where AADOperationType in ("Assign", "AssignEligibleRole", "CreateRequestGrantedRole", "CreateRequestPermanentEligibleRole", "CreateRequestPermanentGrantedRole")

Stage 4: where

| where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")

Stage 5: kusto:mv-apply

| mv-apply TargetResourceSubject = TargetResources on
    (
    where TargetResourceSubject.type in~ ("User", "ServicePrincipal")
    | extend
        Target = iff(TargetResourceSubject.type =~ "ServicePrincipal", tostring(TargetResourceSubject.displayName), tostring(TargetResourceSubject.userPrincipalName)),
        subjectProps = TargetResourceSubject.modifiedProperties
    )

Stage 6: kusto:mv-apply

| mv-apply TargetResourceRole = TargetResources on
    (
    where TargetResourceRole.type in~ ("Role")
    | extend roleProps = pack_array(bag_pack("displayName", "Role.DisplayName", "newValue", TargetResourceRole.displayName))
    )

Stage 7: kusto:mv-apply

| mv-apply Property = iff(array_length(subjectProps) > 0, subjectProps, roleProps) on
    (
    where Property.displayName =~ "Role.DisplayName"
    | extend RoleName = trim('"', tostring(Property.newValue))
    )

Stage 8: where

| where RoleName contains "Admin"

Stage 9: extend (6 consecutive steps)

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

Stage 10: where

| where Initiator != "MS-PIM"

Stage 11: summarize

| summarize
    by
    bin(TimeGenerated, 1h),
    OperationName,
    RoleName,
    Target,
    Initiator,
    InitiatingUserPrincipalName,
    InitiatingAadUserId,
    InitiatingAppName,
    InitiatingAppServicePrincipalId,
    InitiatingIpAddress,
    Result

Stage 12: extend

| 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])

Stage 13: extend

| extend
    Source_Network_IPLocation = "",
    ActivityType=OperationName

Stage 14: project

| project
        Alert_Category_en = "Entra ID",
    Alert_SubCategory_en = "Anomaly Identity Privilege Modification",
    Alert_Name_en = "User Assigned a Privileged Role",
    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, "<UnknownInitiator>"),
                           ", from IP: ",
                           InitiatingIpAddress,
                           ", assigned privileged role  ",
                           tostring(RoleName),
                           "  to user  ",
                           iff(isnotempty(Target), Target, "<NoTargetUser>"),
                           ". This behavior is a high-risk permission change and must be validated immediately."
                       ),
    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 this is determined to be an unauthorized role assignment, immediately revoke the privileged role from target user  ",
                           iff(isnotempty(Target), Target, "<NoTargetUser>"),
                           " 's privileged role (",
                           tostring(RoleName),
                           "), and restore the state to before the change.",
                           "2. Immediately restrict or suspend the operator that performed the role assignment (user or application), revoke sign-in tokens, and force MFA/password reset as needed.  ",
                           "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,
    Source_Identity_FullName = iff(isnotempty(Initiator), Initiator, InitiatingAppName),
    Source_Identity_Type = iff(isnotempty(InitiatingUserPrincipalName), "User", "Service"),
    Source_Network_IPAddress = InitiatingIpAddress,
    Source_Network_IPLocation = Source_Network_IPLocation,
    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"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
AADOperationTypein
  • Assign
  • AssignEligibleRole
  • CreateRequestGrantedRole
  • CreateRequestPermanentEligibleRole
  • CreateRequestPermanentGrantedRole
field:"AADOperationType" kind:in
ActivityDisplayNamematch
  • Add eligible member to role transforms: term
  • Add member to role transforms: term
field:"azure_ad::activity_display_name" kind:match
Categoryeq
  • RoleManagement
field:"Category" kind:eq value:"RoleManagement"
Initiatorne
  • MS-PIM
field:"Initiator" kind:ne value:"MS-PIM"
RoleNamecontains
  • Admin
field:"RoleName" kind:contains value:"Admin"
displayNameeq
  • Role.DisplayName
field:"displayName" kind:eq value:"Role.DisplayName"
typein
  • Role
  • ServicePrincipal
  • User
field:"type" kind:in

Output fields

These fields are emitted when the rule matches.

FieldSource
Alert_Category_enproject
Alert_Containment_enproject
Alert_Description_enproject
Alert_Name_enproject
Alert_Remediation_enproject
Alert_SubCategory_enproject
Alert_Time_TWproject
Alert_Time_UTC0project
Alert_TriageStep_enproject
Event_Actionproject
Event_Statusproject
Source_Identity_FullNameproject
Source_Identity_Typeproject
Source_Network_IPAddressproject
Source_Network_IPLocationproject
Target_Identity_DomainTypeproject
Target_Identity_FullNameproject
Target_Identity_Typeproject
Target_Resource_IDproject
Target_Resource_Typeproject