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.

Query · kql

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
Raw source Privileged directory role assigned outside PIM workflow · KQL
Esc
Published by Azure/Azure-Sentinel ↗, licensed under MIT ↗. Reproduced here unmodified.
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
version: 1.0.0
metadata:
    source:
        kind: Community
    author:
        name: descambiado
    support:
        tier: Community
    categories:
        domains: [ "Security - Threat Protection", "Identity" ]

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.