Query Details
// This query can help you to list application consents and assignments in Entra ID.
//
// Click "Save as function", in Parameters write in the fields:
// "timespan" "query_frequency" "14d"
// "timespan" "query_wait" "1h"
//
// If you name the function "AppConsentAssignment", you can check the function with queries like the following:
//
// AppConsentAssignment()
//
// AppConsentAssignment(1h, 1h)
//
// AppConsentAssignment(14d, 1h)
//
// let query_frequency = 14d;
// let query_period = query_frequency + query_wait;
// let query_wait = 1h;
//let Function = (query_frequency:timespan = 14d, query_wait:timespan = 1h){
let _ConsentRiskDictionary = toscalar(
_GetWatchlist("Permission-MSAppPermissions")
| summarize Permissions = make_bag(bag_pack(PermissionName, ConsentRisk)) by PermissionAPI
| summarize make_bag(bag_pack(PermissionAPI, Permissions))
);
let _ConsentToApplication =
AuditLogs
| where TimeGenerated > ago(query_frequency + query_wait)
| where LoggedByService == "Core Directory" and Category == "ApplicationManagement" and OperationName has "Consent to application"
| extend
Actor = tostring(coalesce(InitiatedBy["user"]["userPrincipalName"], InitiatedBy["app"]["displayName"])),
ActorId = tostring(coalesce(InitiatedBy["user"]["id"], InitiatedBy["app"]["servicePrincipalId"])),
ActorIPAddress = tostring(coalesce(InitiatedBy["user"]["ipAddress"], InitiatedBy["app"]["ipAddress"])),
AppDisplayName = tostring(TargetResources[0]["displayName"]),
AppServicePrincipalId = tostring(TargetResources[0]["id"])
| mv-apply ModifiedProperty = TargetResources[0]["modifiedProperties"] on (
summarize BagToUnpack = make_bag(bag_pack(tostring(ModifiedProperty["displayName"]), ModifiedProperty["newValue"]))
)
| evaluate bag_unpack(BagToUnpack, columnsConflict="replace_source")
| extend
AdminConsent = trim(@'[\"\s]+', tostring(column_ifexists("ConsentContext.IsAdminConsent", dynamic(null)))),
IsAppOnly = trim(@'[\"\s]+', tostring(column_ifexists("ConsentContext.IsAppOnly", dynamic(null)))),
OnBehalfOfAllUsers = trim(@'[\"\s]+', tostring(column_ifexists("ConsentContext.OnBehalfOfAll", dynamic(null)))),
Tags = trim(@'[\"\s]+', tostring(column_ifexists("ConsentContext.Tags", dynamic(null)))),
Permissions = extract_all(@"PrincipalId: ([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12})?, ResourceId: ([a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}), ConsentType:\s+(\w+), Scope:\s+([^,]+)", extract(@"\=\>\s+(.*)", 1, tostring(column_ifexists("ConsentAction.Permissions", ""))))
| mv-apply Permissions on (
extend
TargetId = tostring(Permissions[0]),
PermissionsResourceId = tostring(Permissions[1]),
ConsentType = tostring(Permissions[2]),
Scope = split(Permissions[3], ' ')
| mv-expand Scope
| summarize Permissions = array_sort_asc(make_set(Scope)) by ConsentType, TargetId, PermissionsResourceId
)
// | mv-apply ServicePrincipalName = split(trim(@'[\"\s]+', tostring(column_ifexists("TargetId.ServicePrincipalNames", dynamic(null)))), ";") on (
// where ServicePrincipalName matches regex @"^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$"
// | summarize AppId = tostring(make_set(ServicePrincipalName)[0])
// )
| extend Target = iff(TargetId == ActorId, Actor, "")
| project
TimeGenerated,
OperationName,
Result,
ResultReason,
Actor,
ActorId,
ActorIPAddress,
AppDisplayName,
// AppId,
AppServicePrincipalId,
Target,
TargetId,
AdminConsent,
IsAppOnly,
OnBehalfOfAllUsers,
Tags,
ConsentType,
Permissions,
PermissionsResourceId,
InitiatedBy,
AdditionalDetails,
TargetResources,
CorrelationId
;
let _DelegatedPermissionGrant =
AuditLogs
| where TimeGenerated > ago(query_frequency + query_wait)
| where LoggedByService == "Core Directory" and Category == "ApplicationManagement" and OperationName has "Add delegated permission grant"
| extend
Actor = tostring(coalesce(InitiatedBy["user"]["userPrincipalName"], InitiatedBy["app"]["displayName"])),
ActorId = tostring(coalesce(InitiatedBy["user"]["id"], InitiatedBy["app"]["servicePrincipalId"])),
ActorIPAddress = tostring(coalesce(InitiatedBy["user"]["ipAddress"], InitiatedBy["app"]["ipAddress"]))
| mv-expand TargetResource = TargetResources
| where array_length(TargetResource["modifiedProperties"]) > 0
| extend
PermissionsResourceDisplayName = tostring(TargetResource["displayName"]),
PermissionsResourceId = tostring(TargetResource["id"])
| mv-apply ModifiedProperty = TargetResource["modifiedProperties"] on (
summarize BagToUnpack = make_bag(bag_pack(tostring(ModifiedProperty["displayName"]), ModifiedProperty["newValue"]))
)
| evaluate bag_unpack(BagToUnpack, columnsConflict="replace_source")
| extend
ConsentType = trim(@'[\"\s]+', tostring(column_ifexists("DelegatedPermissionGrant.ConsentType", dynamic(null)))),
Permissions = array_sort_asc(split(trim(@'[\"\s]+', tostring(column_ifexists("DelegatedPermissionGrant.Scope", dynamic(null)))), " ")),
AppServicePrincipalId = trim(@'[\"\s]+', tostring(column_ifexists("ServicePrincipal.ObjectID", dynamic(null))))
// | mv-apply ServicePrincipalName = split(trim(@'[\"\s]+', tostring(column_ifexists("TargetId.ServicePrincipalNames", dynamic(null)))), ";") on (
// where ServicePrincipalName matches regex @"^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$"
// | summarize AppId = tostring(make_set(ServicePrincipalName)[0])
// )
| extend
Target = iff(ConsentType == "Principal", Actor, ""),
TargetId = iff(ConsentType == "Principal", ActorId, "")
| project
TimeGenerated,
OperationName,
Result,
ResultReason,
Actor,
ActorId,
ActorIPAddress,
// AppId,
AppServicePrincipalId,
Target,
TargetId,
ConsentType,
Permissions,
PermissionsResourceDisplayName,
PermissionsResourceId,
InitiatedBy,
AdditionalDetails,
TargetResources,
CorrelationId
;
let _AppRoleAssignmentUser =
AuditLogs
| where TimeGenerated > ago(query_frequency + query_wait)
| where LoggedByService == "Core Directory" and Category == "UserManagement" and OperationName has "Add app role assignment grant to user"
| where not(Result == "failure" and ResultDescription == "Microsoft.Online.DirectoryServices.UniqueKeyPropertyException")
| extend
Actor = tostring(coalesce(InitiatedBy["user"]["userPrincipalName"], InitiatedBy["app"]["displayName"])),
ActorId = tostring(coalesce(InitiatedBy["user"]["id"], InitiatedBy["app"]["servicePrincipalId"])),
ActorIPAddress = tostring(coalesce(InitiatedBy["user"]["ipAddress"], InitiatedBy["app"]["ipAddress"]))
| mv-expand TargetResource = TargetResources
| where array_length(TargetResource["modifiedProperties"]) > 0
| extend
AppDisplayName = tostring(TargetResource["displayName"]),
AppServicePrincipalId = tostring(TargetResource["id"])
| mv-apply Properties = TargetResource["modifiedProperties"] on (
summarize BagToUnpack = make_bag(bag_pack(tostring(Properties["displayName"]), Properties["newValue"]))
)
| evaluate bag_unpack(BagToUnpack, columnsConflict="replace_source")
// | mv-apply ServicePrincipalName = split(trim(@'[\"\s]+', tostring(column_ifexists("TargetId.ServicePrincipalNames", dynamic(null)))), ";") on (
// where ServicePrincipalName matches regex @"^[a-f0-9]{8}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{4}\-[a-f0-9]{12}$"
// | summarize AppId = tostring(make_set(ServicePrincipalName)[0])
// )
| extend
Target = trim(@'[\"\s]+', tostring(column_ifexists("User.UPN", dynamic(null)))),
TargetId = trim(@'[\"\s]+', tostring(column_ifexists("User.ObjectID", dynamic(null)))),
AppRoleDisplayName = trim(@'[\"\s]+', tostring(column_ifexists("AppRole.DisplayName", dynamic(null)))),
AppRoleId = trim(@'[\"\s]+', tostring(column_ifexists("AppRole.Id", dynamic(null))))
| project
TimeGenerated,
OperationName,
Result,
ResultReason,
Actor,
ActorId,
ActorIPAddress,
// AppId,
AppDisplayName,
AppServicePrincipalId,
AppRoleDisplayName,
AppRoleId,
Target,
TargetId,
InitiatedBy,
AdditionalDetails,
TargetResources,
CorrelationId
;
let _AppRoleAssignmentServicePrincipal =
AuditLogs
| where TimeGenerated > ago(query_frequency + query_wait)
| where LoggedByService == "Core Directory" and Category == "ApplicationManagement" and OperationName has "Add app role assignment to service principal"
| where not(Result == "failure" and ResultDescription == "Microsoft.Online.DirectoryServices.UniqueKeyPropertyException")
| extend
Actor = tostring(coalesce(InitiatedBy["user"]["userPrincipalName"], InitiatedBy["app"]["displayName"])),
ActorId = tostring(coalesce(InitiatedBy["user"]["id"], InitiatedBy["app"]["servicePrincipalId"])),
ActorIPAddress = tostring(coalesce(InitiatedBy["user"]["ipAddress"], InitiatedBy["app"]["ipAddress"]))
| mv-expand TargetResource = TargetResources
| where array_length(TargetResource["modifiedProperties"]) > 0
| extend
PermissionsResourceDisplayName = tostring(TargetResource["displayName"]),
PermissionsResourceId = tostring(TargetResource["id"])
| mv-apply ModifiedProperty = TargetResource["modifiedProperties"] on (
summarize BagToUnpack = make_bag(bag_pack(tostring(ModifiedProperty["displayName"]), ModifiedProperty["newValue"]))
)
| evaluate bag_unpack(BagToUnpack, columnsConflict="replace_source")
| extend
TargetAppDisplayName = trim(@'[\"\s]+', tostring(column_ifexists("ServicePrincipal.DisplayName", dynamic(null)))),
TargetAppId = trim(@'[\"\s]+', tostring(column_ifexists("ServicePrincipal.AppId", dynamic(null)))),
TargetAppServicePrincipalId = trim(@'[\"\s]+', tostring(column_ifexists("ServicePrincipal.ObjectID", dynamic(null)))),
Permission = trim(@'[\"\s]+', tostring(column_ifexists("AppRole.Value", dynamic(null)))),
PermissionId = trim(@'[\"\s]+', tostring(column_ifexists("AppRole.Id", dynamic(null)))),
PermissionDisplayText = trim(@'[\"\s]+', tostring(column_ifexists("AppRole.DisplayName", dynamic(null))))
| summarize Permissions = array_sort_asc(make_set(Permission)), arg_min(TimeGenerated, *) by CorrelationId, TargetAppServicePrincipalId, Result, PermissionsResourceId
| extend
Target = TargetAppDisplayName,
TargetId = TargetAppServicePrincipalId
| project
TimeGenerated,
OperationName,
Result,
ResultReason,
Actor,
ActorId,
ActorIPAddress,
Target,
TargetId,
TargetAppId,
Permissions,
PermissionsResourceDisplayName,
PermissionsResourceId,
InitiatedBy,
AdditionalDetails,
TargetResources,
CorrelationId
;
union isfuzzy=true
_AppRoleAssignmentServicePrincipal,
_AppRoleAssignmentUser,
_DelegatedPermissionGrant,
_ConsentToApplication
| project
TimeGenerated,
OperationName,
Result,
ResultReason,
Actor,
ActorId,
ActorIPAddress,
// AppId,
AppDisplayName,
AppServicePrincipalId,
AppRoleDisplayName,
AppRoleId,
Target,
TargetId,
TargetAppId,
AdminConsent,
IsAppOnly,
OnBehalfOfAllUsers,
Tags,
ConsentType,
Permissions,
PermissionsResourceDisplayName,
PermissionsResourceId,
InitiatedBy,
AdditionalDetails,
TargetResources,
CorrelationId
| as _Results
| project-away PermissionsResourceDisplayName
| lookup kind=leftouter (
_Results
| distinct PermissionsResourceDisplayName, PermissionsResourceId
| where isnotempty(PermissionsResourceDisplayName) and isnotempty(PermissionsResourceId)
) on PermissionsResourceId
| extend PermissionsResourceDisplayName = coalesce(PermissionsResourceDisplayName, PermissionsResourceId)
| mv-apply Permission = Permissions to typeof(string) on (
where isnotempty(Permission)
| extend ConsentRisk = case(
bag_has_key(_ConsentRiskDictionary[PermissionsResourceDisplayName], Permission), coalesce(tostring(_ConsentRiskDictionary[PermissionsResourceDisplayName][Permission]), "UnknownRisk"),
not(bag_has_key(_ConsentRiskDictionary, PermissionsResourceDisplayName)), "UnknownPermissionResource",
"UnknownPermission"
)
| summarize
ConsentRisks = make_set(ConsentRisk),
PermissionsExtended = make_bag(bag_pack(Permission, ConsentRisk)) by PermissionsResourceDisplayName
)
| extend
ConsentRisks = iff(isempty(Permissions), dynamic(null), ConsentRisks),
Permissions = iff(isempty(Permissions), dynamic(null), PermissionsExtended)
| project-away PermissionsExtended
| extend Auxiliar = case(
OperationName == "Add app role assignment to service principal", CorrelationId,
"")
| summarize
ConsentRisks = make_set(ConsentRisks),
Permissions = make_bag(bag_pack(PermissionsResourceDisplayName, Permissions)),
arg_min(TimeGenerated, *)
by Auxiliar, CorrelationId, ActorId, OperationName, TargetId, ConsentType, AppServicePrincipalId
| project-away *1, PermissionsResourceId, PermissionsResourceDisplayName
| summarize
StartTime = min(TimeGenerated),
EndTime = max(TimeGenerated),
Operations = make_bag(bag_pack(OperationName, pack_array(Result, ResultReason))),
take_any(AppDisplayName),
take_any(Actor, ActorIPAddress),
take_any(Target, TargetAppId),
take_anyif(AdminConsent, not(AdminConsent in ("", "False"))),
take_anyif(OnBehalfOfAllUsers, not(OnBehalfOfAllUsers in ("", "False"))),
take_anyif(ConsentType, isnotempty(ConsentType)),
Permissions = make_bag(bag_pack(OperationName, Permissions)),
ConsentRisks = array_sort_asc(make_set_if(ConsentRisks, isnotempty(ConsentRisks))),
InitiatedBy = make_bag(bag_pack(OperationName, InitiatedBy)),
AdditionalDetails = make_bag(bag_pack(OperationName, AdditionalDetails)),
TargetResources = make_bag(bag_pack(OperationName, TargetResources))
by Auxiliar, CorrelationId, ActorId, AppServicePrincipalId, TargetId
| where StartTime between (ago(query_frequency + query_wait) .. ago(query_wait))
| project-away Auxiliar
| extend AlertSeverity = case(
ConsentRisks has_any ("UnknownPermissionResource", "UnknownPermission", "UnknownRisk", "High"), "High",
ConsentRisks has "Medium", "Medium",
ConsentRisks has "Low", "Low",
"High"
)
| project
StartTime,
EndTime,
Operations,
Actor,
ActorId,
ActorIPAddress,
AppDisplayName,
AppServicePrincipalId,
Target,
TargetId,
TargetAppId,
AdminConsent,
OnBehalfOfAllUsers,
ConsentType,
ConsentRisks,
Permissions,
InitiatedBy,
AdditionalDetails,
TargetResources,
CorrelationId,
AlertSeverity
This query is designed to help you list application consents and assignments in Entra ID (formerly Azure Active Directory). Here's a simplified breakdown of what it does:
Parameters and Function Setup:
AppConsentAssignment. It takes two parameters: query_frequency (default 14 days) and query_wait (default 1 hour).Consent Risk Dictionary:
Consent to Application:
Delegated Permission Grant:
App Role Assignment:
Union and Processing:
Consent Risks and Permissions:
Final Output:
Alert Severity:
The query is useful for auditing and monitoring application consents and assignments, helping to identify potential security risks associated with permissions granted to applications in Entra ID.

Jose Sebastián Canós
Released: June 27, 2025
Tables
Keywords
Operators