Detection rules › Kusto

Threat Essentials - NRT User added to Microsoft Entra ID Privileged Groups

Status
available
Severity
medium
Time window
14d
Source
github.com/Azure/Azure-Sentinel

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

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

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 =
ifisnotempty(ServicePrincipalName)ServicePrincipalName
elifisnotempty(ServicePrincipalId)ServicePrincipalId
elifisnotempty(UserPrincipalName)UserPrincipalName
else"not available"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Categoryeq
  • RoleManagement
field:"Category" kind:eq value:"RoleManagement"
GroupNamein
  • PrivilegedRoleAdmins
  • TenantAdmins
  • UserAccountAdmins
field:"GroupName" kind:in
OperationNamein
  • Add member to role
  • Add member to role in PIM requested (permanent)
field:"OperationName" kind:in
displayNameeq
  • Role.WellKnownObjectName
field:"displayName" kind:eq value:"Role.WellKnownObjectName"

Output fields

These fields are emitted when the rule matches.

FieldSource
AADOperationTypeproject
AADTenantIdproject
AppIdproject
Categoryproject
DisplayNameproject
GroupNameproject
InitiatedByDisplayNameproject
OperationNameproject
ServicePrincipalIdproject
ServicePrincipalNameproject
TargetUserPrincipalNameproject
TimeGeneratedproject
UserIPAddressproject
UserIdproject
UserPrincipalNameproject
UserRolesproject
InitiatorAccountextend
InitiatorNameextend
InitiatorUPNSuffixextend
InitiatorAadUserIdextend
TargetNameextend
TargetUPNSuffixextend