Detection rules › Kusto
Service Principal Assigned App Role With Sensitive Access
Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read. A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions. Ensure that any assignment to a Service Principal is valid and appropriate. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Privilege Escalation |
Telemetry coverage
| Platform | Record / event type |
|---|---|
| Azure | Add app role assignment to service principal |
Rules detecting the same action
These rules filter on the same operation.
- [Entra ID] Application Assigned Administrator Permissions Immediately After Obtaining Role Management Permissions (Kusto)
- [Entra ID] Application Granted Administrative Permission to Assign Microsoft Entra ID Roles (Kusto)
- Added Credentials to Existing Application (Sigma)
- Added Owner To Application (Sigma)
- Admin promotion after Role Management Application Permission Grant (Kusto)
- App Granted Microsoft Permissions (Sigma)
- App Granted Privileged Delegated Or App Permissions (Sigma)
- Application AppID Uri Configuration Changes (Sigma)
Rule body
id: dd78a122-d377-415a-afe9-f22e08d2112c
name: Service Principal Assigned App Role With Sensitive Access
description: |
'Detects a Service Principal being assigned an app role that has sensitive access such as Mail.Read.
A threat actor who compromises a Service Principal may assign it an app role to allow it to access sensitive data, or to perform other actions.
Ensure that any assignment to a Service Principal is valid and appropriate.
Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-applications#application-granted-highly-privileged-permissions'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
- PrivilegeEscalation
relevantTechniques:
- T1078.004
tags:
- AADSecOpsGuide
query: |
// Add other permissions to this list as needed
let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
"Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
let auditList =
AuditLogs
| where OperationName =~ "Add app role assignment to service principal"
| mv-expand TargetResources[0].modifiedProperties
| extend TargetResources_0_modifiedProperties = column_ifexists("TargetResources_0_modifiedProperties", '')
| where isnotempty(TargetResources_0_modifiedProperties)
;
let detailsList = auditList
| where TargetResources_0_modifiedProperties.displayName =~ "AppRole.Value" or TargetResources_0_modifiedProperties.displayName =~ "DelegatedPermissionGrant.Scope"
| extend Permissions = split((parse_json(tostring(TargetResources_0_modifiedProperties.newValue))), " ")
| where Permissions has_any (permissions)
| summarize AddedPermissions=make_set(Permissions,200) by CorrelationId
| join kind=inner auditList on CorrelationId
| 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(InitiatedBy.user.ipAddress)
| extend InitiatedBy = tostring(iff(isnotempty(InitiatingUserPrincipalName),InitiatingUserPrincipalName, InitiatingAppName))
| extend displayName = tostring(TargetResources_0_modifiedProperties.displayName), newValue = tostring(parse_json(tostring(TargetResources_0_modifiedProperties.newValue)))
| where displayName == "ServicePrincipal.ObjectID" or displayName == "ServicePrincipal.DisplayName"
| extend displayName = case(displayName == "ServicePrincipal.ObjectID", "ServicePrincipalObjectID", displayName == "ServicePrincipal.DisplayName", "ServicePrincipalDisplayName", displayName)
| project TimeGenerated, CorrelationId, Id, AddedPermissions = tostring(AddedPermissions), InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, displayName, newValue
;
detailsList | project Id, displayName, newValue
| evaluate pivot(displayName, make_set(newValue))
| join kind=inner detailsList on Id
| extend ServicePrincipalObjectID = todynamic(column_ifexists("ServicePrincipalObjectID", "")), ServicePrincipalDisplayName = todynamic(column_ifexists("ServicePrincipalDisplayName", ""))
| mv-expand ServicePrincipalObjectID, ServicePrincipalDisplayName
| project-away Id1, displayName, newValue
| extend ServicePrincipalObjectID = tostring(ServicePrincipalObjectID), ServicePrincipalDisplayName = tostring(ServicePrincipalDisplayName)
| summarize FirstSeen = min(TimeGenerated), LastSeen = max(TimeGenerated), EventIds = make_set(Id,200) by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: InitiatingUserPrincipalName
- identifier: Name
columnName: InitiatingAccountName
- identifier: UPNSuffix
columnName: InitiatingAccountUPNSuffix
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAadUserId
- entityType: Account
fieldMappings:
- identifier: AadUserId
columnName: InitiatingAppServicePrincipalId
- identifier: ObjectGuid
columnName: ServicePrincipalObjectID
- entityType: IP
fieldMappings:
- identifier: Address
columnName: InitiatingIPAddress
version: 2.0.0
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Others", "Identity" ]
Stages and Predicates
Stage 0: let
let permissions = dynamic([".All", "ReadWrite", "Mail.", "offline_access", "Files.Read", "Notes.Read", "ChannelMessage.Read", "Chat.Read", "TeamsActivity.Read",
"Group.Read", "EWS.AccessAsUser.All", "EAS.AccessAsUser.All"]);
let auditList = AuditLogs <inlined as stages below>;
let detailsList = auditList <inlined as stages below>;
Stage 1: source
AuditLogs
Stage 2: where
where OperationName =~ "Add app role assignment to service principal"
Stage 3: mv-expand
mv-expand modifiedProperties
Stage 4: extend
extend TargetResources_0_modifiedProperties
Stage 5: where
where isnotempty(TargetResources_0_modifiedProperties)
Stage 6: where
where (displayName =~ "AppRole.Value" or displayName =~ "DelegatedPermissionGrant.Scope")
Stage 7: extend
extend Permissions
Stage 8: where
where (Permissions contains ".All" or Permissions contains "ReadWrite" or Permissions contains "Mail." or Permissions contains "offline_access" or Permissions contains "Files.Read" or Permissions contains "Notes.Read" or Permissions contains "ChannelMessage.Read" or Permissions contains "Chat.Read" or Permissions contains "TeamsActivity.Read" or Permissions contains "Group.Read" or Permissions contains "EWS.AccessAsUser.All" or Permissions contains "EAS.AccessAsUser.All")
Stage 9: summarize
summarize AddedPermissions by CorrelationId
Stage 10: join
| join kind=inner auditList on CorrelationId
Stage 11: extend (7 consecutive steps)
extend InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, displayName, newValue
Stage 12: where
where (displayName =~ "ServicePrincipal.DisplayName" or displayName =~ "ServicePrincipal.ObjectID")
Stage 13: extend
extend displayName
displayName =if
displayName == "ServicePrincipal.ObjectID""ServicePrincipalObjectID"elif
displayName == "ServicePrincipal.DisplayName""ServicePrincipalDisplayName"else
displayNameStage 14: project
project AddedPermissions, CorrelationId, Id, InitiatedBy, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, TimeGenerated, displayName, newValue
Stage 15: project
project Id, displayName, newValue
Stage 16: evaluate
evaluate
Stage 17: join
join kind=inner (detailsList) on Id
Stage 18: extend
extend ServicePrincipalDisplayName, ServicePrincipalObjectID
Stage 19: mv-expand
mv-expand ServicePrincipalObjectID
Stage 20: project-away
project-away Id1, displayName, newValue
Stage 21: extend
extend ServicePrincipalDisplayName, ServicePrincipalObjectID
Stage 22: summarize
summarize EventIds, FirstSeen, LastSeen by CorrelationId, AddedPermissions, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIPAddress, InitiatingUserPrincipalName, InitiatedBy, ServicePrincipalDisplayName, ServicePrincipalObjectID
Stage 23: extend
extend InitiatingAccountName, InitiatingAccountUPNSuffix
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
OperationName | eq |
| field:"OperationName" kind:eq value:"Add app role assignment to service principal" |
Permissions | match |
| field:"Permissions" kind:match |
TargetResources_0_modifiedProperties | is_not_null | field:"TargetResources_0_modifiedProperties" kind:is_not_null | |
displayName | eq |
| field:"displayName" kind:eq |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
AddedPermissions | summarize |
CorrelationId | summarize |
EventIds | summarize |
FirstSeen | summarize |
InitiatedBy | summarize |
InitiatingAadUserId | summarize |
InitiatingAppName | summarize |
InitiatingAppServicePrincipalId | summarize |
InitiatingIPAddress | summarize |
InitiatingUserPrincipalName | summarize |
LastSeen | summarize |
ServicePrincipalDisplayName | summarize |
ServicePrincipalObjectID | summarize |
InitiatingAccountName | extend |
InitiatingAccountUPNSuffix | extend |