← Back to SOC feed Coverage →

High-privilege application role assigned to service principal

kql MEDIUM Azure-Sentinel
T1098.003T1528
AuditLogs
huntingmicrosoftofficial
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 exploit high-privilege application roles assigned to service principals to gain unauthorized access to sensitive data and directory management capabilities. SOC teams should proactively hunt for this behavior in Azure Sentinel to identify potential privilege escalation and lateral movement opportunities.

KQL Query

let timeframe = 1d;
let HighRiskRoles = dynamic([
    "Mail.ReadWrite",
    "Mail.Send",
    "Files.ReadWrite.All",
    "Directory.ReadWrite.All",
    "RoleManagement.ReadWrite.Directory",
    "Application.ReadWrite.All",
    "AppRoleAssignment.ReadWrite.All",
    "User.ReadWrite.All",
    "full_access_as_app"
]);
AuditLogs
| where TimeGenerated >= ago(timeframe)
| where OperationName =~ "Add app role assignment to service principal"
| 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 TargetSpName = tostring(TargetResources[0].displayName)
| extend TargetSpId   = tostring(TargetResources[0].id)
| mv-expand ModProp = TargetResources[0].modifiedProperties
| where tostring(ModProp.displayName) =~ "AppRole.Value"
| extend AppRoleName = trim('"', tostring(ModProp.newValue))
| where AppRoleName in~ (HighRiskRoles)
| extend AccountName      = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
| extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
| project TimeGenerated, Actor, AccountName, AccountUPNSuffix, ActorIp,
          TargetSpName, TargetSpId, AppRoleName, CorrelationId
| sort by TimeGenerated desc

Analytic Rule Definition

id: 840c673e-a712-49eb-a6a9-6759d5259c4b
name: High-privilege application role assigned to service principal
description: |
  Identifies application role assignments to service principals granting high-risk
  permissions such as Mail.ReadWrite, Directory.ReadWrite.All, or
  RoleManagement.ReadWrite.Directory, which provide tenant-wide API access
  without user context.
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
tactics:
  - Persistence
  - CredentialAccess
relevantTechniques:
  - T1098.003
  - T1528
query: |
  let timeframe = 1d;
  let HighRiskRoles = dynamic([
      "Mail.ReadWrite",
      "Mail.Send",
      "Files.ReadWrite.All",
      "Directory.ReadWrite.All",
      "RoleManagement.ReadWrite.Directory",
      "Application.ReadWrite.All",
      "AppRoleAssignment.ReadWrite.All",
      "User.ReadWrite.All",
      "full_access_as_app"
  ]);
  AuditLogs
  | where TimeGenerated >= ago(timeframe)
  | where OperationName =~ "Add app role assignment to service principal"
  | 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 TargetSpName = tostring(TargetResources[0].displayName)
  | extend TargetSpId   = tostring(TargetResources[0].id)
  | mv-expand ModProp = TargetResources[0].modifiedProperties
  | where tostring(ModProp.displayName) =~ "AppRole.Value"
  | extend AppRoleName = trim('"', tostring(ModProp.newValue))
  | where AppRoleName in~ (HighRiskRoles)
  | extend AccountName      = iff(Actor has "@", tostring(split(Actor, "@")[0]), Actor)
  | extend AccountUPNSuffix = iff(Actor has "@", tostring(split(Actor, "@")[1]), "")
  | project TimeGenerated, Actor, AccountName, AccountUPNSuffix, ActorIp,
            TargetSpName, TargetSpId, AppRoleName, CorrelationId
  | sort by TimeGenerated desc
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Actor
      - 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" ]

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/ApplicationAppRoleAssignedHighPrivilege.yaml