Detection rules › Kusto

Microsoft Entra ID Role Management Permission Grant

Status
available
Severity
high
Time window
2h
Source
github.com/Azure/Azure-Sentinel

Identifies when the Microsoft Graph RoleManagement.ReadWrite.Directory (Delegated or Application) permission is granted to a service principal. This permission allows an application to read and manage the role-based access control (RBAC) settings for your company's directory. An adversary could use this permission to add an Microsoft Entra ID object to an Admin directory role and escalate privileges. Ref : https://docs.microsoft.com/graph/permissions-reference#role-management-permissions Ref : https://docs.microsoft.com/graph/api/directoryrole-post-members?view=graph-rest-1.0&tabs=http

MITRE ATT&CK coverage

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body kusto

id: 1ff56009-db01-4615-8211-d4fda21da02d
name: Microsoft Entra ID Role Management Permission Grant
description: |
  'Identifies when the Microsoft Graph RoleManagement.ReadWrite.Directory (Delegated or Application) permission is granted to a service principal.
  This permission allows an application to read and manage the role-based access control (RBAC) settings for your company's directory.
  An adversary could use this permission to add an Microsoft Entra ID object to an Admin directory role and escalate privileges.
  Ref : https://docs.microsoft.com/graph/permissions-reference#role-management-permissions
  Ref : https://docs.microsoft.com/graph/api/directoryrole-post-members?view=graph-rest-1.0&tabs=http'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 2h
queryPeriod: 2h
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Persistence
  - Impact
relevantTechniques:
  - T1098.003
  - T1078.004
tags:
  - SimuLand
query: |
  AuditLogs
  | where Category =~ "ApplicationManagement" and LoggedByService =~ "Core Directory" and OperationName in~ ("Add delegated permission grant", "Add app role assignment to service principal")
  | mv-apply TargetResource = TargetResources on
    (
        where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
        | extend props = TargetResource.modifiedProperties
    )
  | mv-apply Property = props on
    (
        where Property.displayName in~ ("AppRole.Value","DelegatedPermissionGrant.Scope")
        | extend DisplayName = tostring(Property.displayName), PermissionGrant = trim('"',tostring(Property.newValue))
    )
  | where PermissionGrant has "RoleManagement.ReadWrite.Directory"
  | mv-apply Property = props on
    (
        where Property.displayName =~ "ServicePrincipal.DisplayName"
        | extend TargetAppDisplayName = trim('"',tostring(Property.newValue))
    )
  | mv-apply Property = props on
    (
        where Property.displayName =~ "ServicePrincipal.ObjectID"
        | extend TargetAppServicePrincipalId = trim('"',tostring(Property.newValue))
    )
  | 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))
  | project TimeGenerated, OperationName, Result, PermissionGrant, TargetAppDisplayName, TargetAppServicePrincipalId, InitiatingAppName, InitiatingAppServicePrincipalId,
  InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, TargetResources, AdditionalDetails, CorrelationId
  | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: TargetAppDisplayName
      - identifier: AadUserId
        columnName: TargetAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: InitiatingAppName
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatingAccountName
      - identifier: UPNSuffix
        columnName: InitiatingAccountUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled

Stages and Predicates

Stage 1: source

AuditLogs

Stage 2: where

| where Category =~ "ApplicationManagement" and LoggedByService =~ "Core Directory" and OperationName in~ ("Add delegated permission grant", "Add app role assignment to service principal")

Stage 3: kusto:mv-apply

| mv-apply TargetResource = TargetResources on
  (
      where TargetResource.type =~ "ServicePrincipal" and array_length(TargetResource.modifiedProperties) > 0 and isnotnull(TargetResource.displayName)
      | extend props = TargetResource.modifiedProperties
  )

Stage 4: kusto:mv-apply

| mv-apply Property = props on
  (
      where Property.displayName in~ ("AppRole.Value","DelegatedPermissionGrant.Scope")
      | extend DisplayName = tostring(Property.displayName), PermissionGrant = trim('"',tostring(Property.newValue))
  )

Stage 5: where

| where PermissionGrant has "RoleManagement.ReadWrite.Directory"

Stage 6: kusto:mv-apply

| mv-apply Property = props on
  (
      where Property.displayName =~ "ServicePrincipal.DisplayName"
      | extend TargetAppDisplayName = trim('"',tostring(Property.newValue))
  )

Stage 7: kusto:mv-apply

| mv-apply Property = props on
  (
      where Property.displayName =~ "ServicePrincipal.ObjectID"
      | extend TargetAppServicePrincipalId = trim('"',tostring(Property.newValue))
  )

Stage 8: extend (5 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))

Stage 9: project

| project TimeGenerated, OperationName, Result, PermissionGrant, TargetAppDisplayName, TargetAppServicePrincipalId, InitiatingAppName, InitiatingAppServicePrincipalId,
InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIpAddress, TargetResources, AdditionalDetails, CorrelationId

Stage 10: extend

| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Categoryeq
  • ApplicationManagement
LoggedByServiceeq
  • Core Directory
OperationNamein
  • Add app role assignment to service principal
  • Add delegated permission grant
PermissionGrantmatch
  • RoleManagement.ReadWrite.Directory transforms: term
displayNameeq
  • ServicePrincipal.DisplayName
  • ServicePrincipal.ObjectID
displayNamein
  • AppRole.Value
  • DelegatedPermissionGrant.Scope
displayNameis_not_null
  • (no value, null check)
modifiedPropertiesgt
  • 0 transforms: array_length
typeeq
  • ServicePrincipal

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
AdditionalDetailsproject
CorrelationIdproject
InitiatingAadUserIdproject
InitiatingAppNameproject
InitiatingAppServicePrincipalIdproject
InitiatingIpAddressproject
InitiatingUserPrincipalNameproject
OperationNameproject
PermissionGrantproject
Resultproject
TargetAppDisplayNameproject
TargetAppServicePrincipalIdproject
TargetResourcesproject
TimeGeneratedproject
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend