Detection rules › Kusto

Account Elevated to New Role

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

Detects an account that is elevated to a new role where that account has not had that role in the last 14 days. Role elevations are a key mechanism for gaining permissions, monitoring which users have which roles, and for anomalies in those roles is useful for finding suspicious activity. Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts

MITRE ATT&CK coverage

TacticTechniques
PersistenceT1078.004 Valid Accounts: Cloud Accounts

Event coverage

Rule body kusto

id: c1c66f0b-5531-4a3e-a619-9d2f770ef730
name: Account Elevated to New Role
description: |
  'Detects an account that is elevated to a new role where that account has not had that role in the last 14 days.
    Role elevations are a key mechanism for gaining permissions, monitoring which users have which roles, and for anomalies in those roles is useful for finding suspicious activity.
    Ref: https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#changes-to-privileged-accounts'
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - Persistence
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
    let auditList =
    AuditLogs
    | where TimeGenerated >= ago(14d)
    | where OperationName =~ "Add member to role completed (PIM activation)"
    | where Result =~ "success"
    | extend TargetUserPrincipalName = tostring(TargetResources[2].userPrincipalName)
    | extend displayName = tostring(TargetResources[0].displayName)
    | extend displayName2 = tostring(TargetResources[3].displayName)
    | extend ElevatedRole = iif(displayName =~ "Member", displayName2, displayName)
    ;
    let lookbackList = auditList
    | where TimeGenerated between(ago(14d)..ago(1d))
    ;
    let recentList = auditList
    | where TimeGenerated > ago(1d)
    ;
    let newlyElevated = recentList
    | join kind = leftanti lookbackList on ElevatedRole, TargetUserPrincipalName
    ;
    newlyElevated | project Id, AdditionalDetails
    | mv-expand bagexpansion=array AdditionalDetails
    | evaluate bag_unpack(AdditionalDetails)
    | extend key = column_ifexists("key", ''), value = column_ifexists("value", '')
    | evaluate pivot(key, make_set(value))
    | extend ipaddr = todynamic(column_ifexists("ipaddr", ""))
    | mv-expand ipaddr
    | project Id, InitiatingIPAddress = tostring(ipaddr)
    | join kind=rightouter newlyElevated on Id
    | 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 = iff(isnotempty(tostring(InitiatedBy.user.ipAddress)), tostring(InitiatedBy.user.ipAddress), InitiatingIPAddress)
    | extend ElevatedBy = iff(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
    | extend ElevatedUser = TargetUserPrincipalName
    | extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
    | extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])
    | project-reorder ElevatedUser, ElevatedRole, ResultReason, ElevatedBy, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, TargetUserPrincipalName
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: FullName
        columnName: TargetUserPrincipalName
      - identifier: Name
        columnName: TargetAccountName
      - identifier: UPNSuffix
        columnName: TargetAccountUPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIPAddress
version: 1.1.2
kind: Scheduled

Stages and Predicates

Let binding: lookbackList

let lookbackList = auditList
| where TimeGenerated between(ago(14d)..ago(1d));

Derived from auditList.

The stages below define let newlyElevated (the rule's main pipeline source).

Stage 1: source

AuditLogs

Stage 2: where

| where TimeGenerated >= ago(14d)

Stage 3: where

| where OperationName =~ "Add member to role completed (PIM activation)"

Stage 4: where

| where Result =~ "success"

Stage 5: extend (4 consecutive steps)

| extend TargetUserPrincipalName = tostring(TargetResources[2].userPrincipalName)
| extend displayName = tostring(TargetResources[0].displayName)
| extend displayName2 = tostring(TargetResources[3].displayName)
| extend ElevatedRole = iif(displayName =~ "Member", displayName2, displayName)

Stage 6: where

| where TimeGenerated > ago(1d)

Stage 7: join (negated)

| join kind = leftanti lookbackList on ElevatedRole, TargetUserPrincipalName

The stages below run on newlyElevated (the outer pipeline).

Stage 8: project

newlyElevated
| project Id, AdditionalDetails

Stage 9: mv-expand

| mv-expand bagexpansion=array AdditionalDetails

Stage 10: evaluate

| evaluate bag_unpack(AdditionalDetails)

Stage 11: extend

| extend key = column_ifexists("key", ''), value = column_ifexists("value", '')

Stage 12: evaluate

| evaluate pivot(key, make_set(value))

Stage 13: extend

| extend ipaddr = todynamic(column_ifexists("ipaddr", ""))

Stage 14: mv-expand

| mv-expand ipaddr

Stage 15: project

| project Id, InitiatingIPAddress = tostring(ipaddr)

Stage 16: join

| join kind=rightouter newlyElevated on Id

Stage 17: extend (9 consecutive steps)

| 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 = iff(isnotempty(tostring(InitiatedBy.user.ipAddress)), tostring(InitiatedBy.user.ipAddress), InitiatingIPAddress)
| extend ElevatedBy = iff(isnotempty(InitiatingUserPrincipalName), InitiatingUserPrincipalName, InitiatingAppName)
| extend ElevatedUser = TargetUserPrincipalName
| extend InitiatingAccountName = tostring(split(InitiatingUserPrincipalName, "@")[0]), InitiatingAccountUPNSuffix = tostring(split(InitiatingUserPrincipalName, "@")[1])
| extend TargetAccountName = tostring(split(TargetUserPrincipalName, "@")[0]), TargetAccountUPNSuffix = tostring(split(TargetUserPrincipalName, "@")[1])

Stage 18: project-reorder

| project-reorder ElevatedUser, ElevatedRole, ResultReason, ElevatedBy, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingIPAddress, TargetUserPrincipalName

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
OperationNameeqAdd member to role completed (PIM activation)
Resulteqsuccess

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.

FieldKindValues
OperationNameeq
  • Add member to role completed (PIM activation)
Resulteq
  • success

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.

FieldSource
Idproject
InitiatingIPAddressextend
InitiatingAppNameextend
InitiatingAppServicePrincipalIdextend
InitiatingUserPrincipalNameextend
InitiatingAadUserIdextend
ElevatedByextend
ElevatedUserextend
InitiatingAccountNameextend
InitiatingAccountUPNSuffixextend
TargetAccountNameextend
TargetAccountUPNSuffixextend