Detection rules › Kusto
Threat Essentials - NRT User added to Microsoft Entra ID Privileged Groups
This will alert when a user is added to any of the Privileged Groups. For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities. For Administrator role permissions in Microsoft Entra ID please see https://docs.microsoft.com/azure/active-directory/users-groups-roles/directory-assign-admin-roles
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Persistence | T1078 Valid Accounts, T1098 Account Manipulation |
| Privilege Escalation | T1078 Valid Accounts, T1098 Account Manipulation |
Event coverage
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- Admin promotion after Role Management Application Permission Grant (Kusto)
- Bulk Changes to Privileged Account Permissions (Kusto)
- Changes to PIM Settings (Kusto)
- Detect PIM Alert Disabling activity (Kusto)
- Multiple admin membership removals from newly created admin. (Kusto)
- New External User Granted Admin Role (Kusto)
- NRT Privileged Role Assigned Outside PIM (Kusto)
- NRT User added to Microsoft Entra ID Privileged Groups (Kusto)
Rule body kusto
id: 0a627f29-f0dd-4924-be92-c3d6dac84367
name: Threat Essentials - NRT User added to Microsoft Entra ID Privileged Groups
description: |
'This will alert when a user is added to any of the Privileged Groups.
For further information on AuditLogs please see https://docs.microsoft.com/azure/active-directory/reports-monitoring/reference-audit-activities.
For Administrator role permissions in Microsoft Entra ID please see https://docs.microsoft.com/azure/active-directory/users-groups-roles/directory-assign-admin-roles'
severity: Medium
status: Available
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
- Persistence
- PrivilegeEscalation
relevantTechniques:
- T1098
- T1078
tags:
- DEV-0537
query: |
let OperationList = dynamic(["Add member to role","Add member to role in PIM requested (permanent)"]);
let PrivilegedGroups = dynamic(["UserAccountAdmins","PrivilegedRoleAdmins","TenantAdmins"]);
AuditLogs
//| where LoggedByService =~ "Core Directory"
| where Category =~ "RoleManagement"
| where OperationName in~ (OperationList)
| mv-expand TargetResources
| extend modProps = parse_json(TargetResources).modifiedProperties
| mv-expand bagexpansion=array modProps
| evaluate bag_unpack(modProps)
| extend displayName = column_ifexists("displayName", "NotAvailable"), newValue = column_ifexists("newValue", "NotAvailable")
| where displayName =~ "Role.WellKnownObjectName"
| extend DisplayName = displayName, GroupName = replace('"','',newValue)
| extend initByApp = parse_json(InitiatedBy).app, initByUser = parse_json(InitiatedBy).user
| extend AppId = initByApp.appId,
InitiatedByDisplayName = case(isnotempty(initByApp.displayName), initByApp.displayName, isnotempty(initByUser.displayName), initByUser.displayName, "not available"),
ServicePrincipalId = tostring(initByApp.servicePrincipalId),
ServicePrincipalName = tostring(initByApp.servicePrincipalName),
UserId = initByUser.id,
UserIPAddress = initByUser.ipAddress,
UserRoles = initByUser.roles,
UserPrincipalName = tostring(initByUser.userPrincipalName),
TargetUserPrincipalName = tostring(TargetResources.userPrincipalName)
| where GroupName in~ (PrivilegedGroups)
// If you don't want to alert for operations from PIM, remove below filtering for MS-PIM.
//| where InitiatedByDisplayName != "MS-PIM" and InitiatedByDisplayName != "MS-PIM-Fairfax"
| project TimeGenerated, AADOperationType, Category, OperationName, AADTenantId, AppId, InitiatedByDisplayName, ServicePrincipalId, ServicePrincipalName, DisplayName, GroupName, UserId, UserIPAddress, UserRoles, UserPrincipalName, TargetUserPrincipalName
| extend InitiatorAccount = case(isnotempty(ServicePrincipalName), ServicePrincipalName, isnotempty(ServicePrincipalId), ServicePrincipalId, isnotempty(UserPrincipalName), UserPrincipalName, "not available")
| extend InitiatorName = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',0)[0]),"")
| extend InitiatorUPNSuffix = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',1)[0]),"")
| extend InitiatorAadUserId = iif(InitiatorAccount !has '@',InitiatorAccount,"")
| extend TargetName = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',0)[0]),"")
| extend TargetUPNSuffix = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',1)[0]),"")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: InitiatorName
- identifier: UPNSuffix
columnName: InitiatorUPNSuffix
- identifier: AadUserId
columnName: InitiatorAadUserId
- entityType: Account
fieldMappings:
- identifier: Name
columnName: TargetName
- identifier: UPNSuffix
columnName: TargetUPNSuffix
version: 1.0.4
kind: NRT
Stages and Predicates
Parameters
let OperationList = dynamic(["Add member to role","Add member to role in PIM requested (permanent)"]);
let PrivilegedGroups = dynamic(["UserAccountAdmins","PrivilegedRoleAdmins","TenantAdmins"]);
Stage 1: source
AuditLogs
Stage 2: where
| where Category =~ "RoleManagement"
Stage 3: where
| where OperationName in~ (OperationList)
Stage 4: mv-expand
| mv-expand TargetResources
Stage 5: extend
| extend modProps = parse_json(TargetResources).modifiedProperties
Stage 6: mv-expand
| mv-expand bagexpansion=array modProps
Stage 7: evaluate
| evaluate bag_unpack(modProps)
Stage 8: extend
| extend displayName = column_ifexists("displayName", "NotAvailable"), newValue = column_ifexists("newValue", "NotAvailable")
Stage 9: where
| where displayName =~ "Role.WellKnownObjectName"
Stage 10: extend (3 consecutive steps)
| extend DisplayName = displayName, GroupName = replace('"','',newValue)
| extend initByApp = parse_json(InitiatedBy).app, initByUser = parse_json(InitiatedBy).user
| extend AppId = initByApp.appId,
InitiatedByDisplayName = case(isnotempty(initByApp.displayName), initByApp.displayName, isnotempty(initByUser.displayName), initByUser.displayName, "not available"),
ServicePrincipalId = tostring(initByApp.servicePrincipalId),
ServicePrincipalName = tostring(initByApp.servicePrincipalName),
UserId = initByUser.id,
UserIPAddress = initByUser.ipAddress,
UserRoles = initByUser.roles,
UserPrincipalName = tostring(initByUser.userPrincipalName),
TargetUserPrincipalName = tostring(TargetResources.userPrincipalName)
Stage 11: where
| where GroupName in~ (PrivilegedGroups)
Stage 12: project
| project TimeGenerated, AADOperationType, Category, OperationName, AADTenantId, AppId, InitiatedByDisplayName, ServicePrincipalId, ServicePrincipalName, DisplayName, GroupName, UserId, UserIPAddress, UserRoles, UserPrincipalName, TargetUserPrincipalName
Stage 13: extend (6 consecutive steps)
| extend InitiatorAccount = case(isnotempty(ServicePrincipalName), ServicePrincipalName, isnotempty(ServicePrincipalId), ServicePrincipalId, isnotempty(UserPrincipalName), UserPrincipalName, "not available")
| extend InitiatorName = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',0)[0]),"")
| extend InitiatorUPNSuffix = iif(InitiatorAccount has '@',tostring(split(InitiatorAccount,'@',1)[0]),"")
| extend InitiatorAadUserId = iif(InitiatorAccount !has '@',InitiatorAccount,"")
| extend TargetName = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',0)[0]),"")
| extend TargetUPNSuffix = iif(TargetUserPrincipalName has '@',tostring(split(TargetUserPrincipalName,'@',1)[0]),"")
InitiatorAccount =isnotempty(ServicePrincipalName)ServicePrincipalNameisnotempty(ServicePrincipalId)ServicePrincipalIdisnotempty(UserPrincipalName)UserPrincipalName"not available"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.
| Field | Kind | Values |
|---|---|---|
Category | eq |
|
GroupName | in |
|
OperationName | in |
|
displayName | eq |
|
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.
| Field | Source |
|---|---|
AADOperationType | project |
AADTenantId | project |
AppId | project |
Category | project |
DisplayName | project |
GroupName | project |
InitiatedByDisplayName | project |
OperationName | project |
ServicePrincipalId | project |
ServicePrincipalName | project |
TargetUserPrincipalName | project |
TimeGenerated | project |
UserIPAddress | project |
UserId | project |
UserPrincipalName | project |
UserRoles | project |
InitiatorAccount | extend |
InitiatorName | extend |
InitiatorUPNSuffix | extend |
InitiatorAadUserId | extend |
TargetName | extend |
TargetUPNSuffix | extend |