← Back to SOC feed Coverage →

Privileged directory role assigned outside PIM workflow

kql MEDIUM Azure-Sentinel
T1098.003
AuditLogs
evasionhuntingmicrosoftofficial
This rule was pulled from an open-source repository and enriched with AI. Validate in a test environment before deploying to production.
View original rule at Azure-Sentinel →
Retrieved: 2026-05-21T23:00:01Z · Confidence: medium

Hunt Hypothesis

Adversaries may bypass Privileged Identity Management controls by directly assigning privileged directory roles, circumventing necessary approval and justification processes. SOC teams should proactively hunt for this behavior to detect potential unauthorized elevation of privileges and mitigate insider threats in Azure Sentinel.

KQL Query

let timeframe = 14d;
let PrivilegedRoles = dynamic([
    "Global Administrator",
    "Privileged Role Administrator",
    "Application Administrator",
    "Cloud Application Administrator",
    "Exchange Administrator",
    "SharePoint Administrator",
    "User Account Administrator",
    "Authentication Administrator",
    "Privileged Authentication Administrator",
    "Security Administrator",
    "Hybrid Identity Administrator"
]);
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where Category =~ "RoleManagement"
| where OperationName =~ "Add member to role."
| where Result =~ "success"
| extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
| extend ActorApp = tostring(InitiatedBy.app.displayName)
| extend Actor    = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
| extend ActorIp  = iff(
      isnotempty(tostring(InitiatedBy.user.ipAddress)),
      tostring(InitiatedBy.user.ipAddress),
      tostring(InitiatedBy.app.ipAddress))
| extend TargetUpn = tostring(TargetResources[0].userPrincipalName)
| extend TargetId  = tostring(TargetResources[0].id)
| mv-expand ModProp = TargetResources[0].modifiedProperties
| where tostring(ModProp.displayName) =~ "Role.DisplayName"
| extend RoleName = trim('"', tostring(ModProp.newValue))
| where RoleName in~ (PrivilegedRoles)
| extend ActorName        = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), Actor)
| extend ActorUPNSuffix   = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
| extend AccountName      = iff(TargetUpn has "@", tostring(split(TargetUpn, "@")[0]), TargetUpn)
| extend AccountUPNSuffix = iff(TargetUpn has "@", tostring(split(TargetUpn, "@")[1]), "")
| project TimeGenerated, Actor, ActorName, ActorUPNSuffix, ActorIp,
          TargetUpn, AccountName, AccountUPNSuffix, TargetId, RoleName, CorrelationId
| sort by TimeGenerated desc

Analytic Rule Definition

id: 2df6ff4f-f90f-4158-ac4a-98c1b23d9e18
name: Privileged directory role assigned outside PIM workflow
description: |
  Identifies permanent directory role assignments to privileged roles made outside
  the Privileged Identity Management activation workflow. Direct assignments bypass
  PIM approval and justification requirements.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - PrivilegeEscalation
relevantTechniques:
  - T1098.003
query: |
  let timeframe = 14d;
  let PrivilegedRoles = dynamic([
      "Global Administrator",
      "Privileged Role Administrator",
      "Application Administrator",
      "Cloud Application Administrator",
      "Exchange Administrator",
      "SharePoint Administrator",
      "User Account Administrator",
      "Authentication Administrator",
      "Privileged Authentication Administrator",
      "Security Administrator",
      "Hybrid Identity Administrator"
  ]);
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where Category =~ "RoleManagement"
  | where OperationName =~ "Add member to role."
  | where Result =~ "success"
  | extend ActorUpn = tostring(InitiatedBy.user.userPrincipalName)
  | extend ActorApp = tostring(InitiatedBy.app.displayName)
  | extend Actor    = iff(isnotempty(ActorUpn), ActorUpn, ActorApp)
  | extend ActorIp  = iff(
        isnotempty(tostring(InitiatedBy.user.ipAddress)),
        tostring(InitiatedBy.user.ipAddress),
        tostring(InitiatedBy.app.ipAddress))
  | extend TargetUpn = tostring(TargetResources[0].userPrincipalName)
  | extend TargetId  = tostring(TargetResources[0].id)
  | mv-expand ModProp = TargetResources[0].modifiedProperties
  | where tostring(ModProp.displayName) =~ "Role.DisplayName"
  | extend RoleName = trim('"', tostring(ModProp.newValue))
  | where RoleName in~ (PrivilegedRoles)
  | extend ActorName        = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[0]), Actor)
  | extend ActorUPNSuffix   = iff(ActorUpn has "@", tostring(split(ActorUpn, "@")[1]), "")
  | extend AccountName      = iff(TargetUpn has "@", tostring(split(TargetUpn, "@")[0]), TargetUpn)
  | extend AccountUPNSuffix = iff(TargetUpn has "@", tostring(split(TargetUpn, "@")[1]), "")
  | project TimeGenerated, Actor, ActorName, ActorUPNSuffix, ActorIp,
            TargetUpn, AccountName, AccountUPNSuffix, TargetId, RoleName, CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Actor
      - identifier: Name
        columnName: ActorName
      - identifier: UPNSuffix
        columnName: ActorUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: TargetUpn
      - identifier: Name
        columnName: AccountName
      - identifier: UPNSuffix
        columnName: AccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: ActorIp

Required Data Sources

Sentinel TableNotes
AuditLogsEnsure this data connector is enabled

MITRE ATT&CK Context

References

False Positive Guidance

Original source: https://github.com/Azure/Azure-Sentinel/blob/main/Hunting Queries/AuditLogs/DirectoryRoleAssignedOutsidePIM.yaml