Detection rules › Kusto

New User Assigned to Privileged Role

Status
available
Severity
high
Time window
14d
Group by
OperationName, RoleName, Target
Source
github.com/Azure/Azure-Sentinel

Identifies when a privileged role is assigned to a new user. Any account eligible for a role is now being given privileged access. If the assignment is unexpected or into a role that isn't the responsibility of the account holder, investigate.

MITRE ATT&CK coverage

TacticTechniques
Persistence

Telemetry coverage

Rules detecting the same action

These rules filter on the same operation.

Rule body

id: 050b9b3d-53d0-4364-a3da-1b678b8211ec
name: New User Assigned to Privileged Role
description: |
  Identifies when a privileged role is assigned to a new user. Any account eligible for a role is now being given privileged access. If the assignment is unexpected or into a role that isn't the responsibility of the account holder, investigate.
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AuditLogs
queryFrequency: 1h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Persistence
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  // Define the start and end times based on input values
  let starttime = now()-1h;
  let endtime = now();
  // Set a lookback period of 14 days
  let lookback = starttime - 14d;
  // Define a reusable function to query audit logs
  let awsFunc = (start:datetime, end:datetime) {
    AuditLogs
    | where TimeGenerated between (start..end)
    | where Category =~ "RoleManagement"
    | where AADOperationType in ("Assign", "AssignEligibleRole")
    | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
    | mv-apply TargetResource = TargetResources on
      (
        where TargetResource.type in~ ("User", "ServicePrincipal")
        | extend Target = iff(TargetResource.type =~ "ServicePrincipal", tostring(TargetResource.displayName), tostring(TargetResource.userPrincipalName)),
        props = TargetResource.modifiedProperties
      )
    | mv-apply Property = props on
      (
        where Property.displayName =~ "Role.DisplayName"
        | extend RoleName = trim('"', tostring(Property.newValue))
      )
    | where RoleName contains "Admin" and Result == "success"
  };
  // Query for audit events in the current day
  let EventInfo_CurrentDay = awsFunc(starttime, endtime);
  // Query for audit events in the historical period (lookback)
  let EventInfo_historical = awsFunc(lookback, starttime);
  // Find unseen events by performing a left anti-join
  let EventInfo_Unseen = (EventInfo_CurrentDay
    | join kind=leftanti(EventInfo_historical) on Target, RoleName, OperationName
  );
  // Extend and clean up the results
  EventInfo_Unseen
  | 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(iff(isnotempty(InitiatedBy.user.ipAddress), InitiatedBy.user.ipAddress, InitiatedBy.app.ipAddress))
  | extend Initiator = iif(isnotempty(InitiatingAppName), InitiatingAppName, InitiatingUserPrincipalName)
  // You can uncomment the lines below to filter out PIM activations
  // | where Initiator != "MS-PIM" and Initiator != "MS-PIM-Fairfax"
  // | summarize StartTime=min(TimeGenerated), EndTime=min(TimeGenerated) by OperationName, RoleName, Target, Initiator, Result
  // Project specific columns and split them for further analysis
  | project TimeGenerated, OperationName, RoleName, Target, Initiator, InitiatingUserPrincipalName, InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, Result
  | extend TargetName = tostring(split(Target,'@',0)[0]), TargetUPNSuffix = tostring(split(Target,'@',1)[0]), InitiatorName = tostring(split(InitiatingUserPrincipalName,'@',0)[0]), InitiatorUPNSuffix = tostring(split(InitiatingUserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Target
      - identifier: Name
        columnName: TargetName
      - identifier: UPNSuffix
        columnName: TargetUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: InitiatingUserPrincipalName
      - identifier: Name
        columnName: InitiatorName
      - identifier: UPNSuffix
        columnName: InitiatorUPNSuffix
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAadUserId
  - entityType: Account
    fieldMappings:
      - identifier: AadUserId
        columnName: InitiatingAppServicePrincipalId
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: InitiatingIpAddress
version: 1.1.1
kind: Scheduled

Stages and Predicates

Parameters

let starttime = now()-1h;
let endtime = now();
let lookback = starttime - 14d;
let EventInfo_CurrentDay = awsFunc(starttime, endtime);
let EventInfo_historical = awsFunc(lookback, starttime);

let EventInfo_Unseen is inlined into the numbered stages below.

Let binding: awsFunc

let awsFunc = (start:datetime, end:datetime) {
  AuditLogs
  | where TimeGenerated between (start..end)
  | where Category =~ "RoleManagement"
  | where AADOperationType in ("Assign", "AssignEligibleRole")
  | where ActivityDisplayName has_any ("Add eligible member to role", "Add member to role")
  | mv-apply TargetResource = TargetResources on
    (
      where TargetResource.type in~ ("User", "ServicePrincipal")
      | extend Target = iff(TargetResource.type =~ "ServicePrincipal", tostring(TargetResource.displayName), tostring(TargetResource.userPrincipalName)),
      props = TargetResource.modifiedProperties
    )
  | mv-apply Property = props on
    (
      where Property.displayName =~ "Role.DisplayName"
      | extend RoleName = trim('"', tostring(Property.newValue))
    )
  | where RoleName contains "Admin" and Result == "success"
};

Stage 1: source

AuditLogs

Stage 2: where

where TimeGenerated >= "start" and TimeGenerated <= "end"

Stage 3: where

where Category =~ "RoleManagement"

Stage 4: where

where AADOperationType in~ ("Assign", "AssignEligibleRole")

Stage 5: where

where (ActivityDisplayName contains "Add eligible member to role" or ActivityDisplayName contains "Add member to role")

Stage 6: kusto:mv-apply

kusto:mv-apply type in~ ("ServicePrincipal", "User")

Stage 7: kusto:mv-apply

kusto:mv-apply displayName =~ "Role.DisplayName"

Stage 8: where

where Result =~ "success" and RoleName contains "Admin"

Stage 9: join (negated)

join kind=leftanti (EventInfo_historical) on Target, RoleName, OperationName

Stage 10: extend (6 consecutive steps)

extend InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator

Stage 11: project

project InitiatingAadUserId, InitiatingAppName, InitiatingAppServicePrincipalId, InitiatingIpAddress, InitiatingUserPrincipalName, Initiator, OperationName, Result, RoleName, Target, TimeGenerated

Stage 12: extend

extend InitiatorName, InitiatorUPNSuffix, TargetName, TargetUPNSuffix

Exclusions

The rule actively suppresses these predicates.

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
InitiatingAadUserIdproject
InitiatingAppNameproject
InitiatingAppServicePrincipalIdproject
InitiatingIpAddressproject
InitiatingUserPrincipalNameproject
Initiatorproject
OperationNameproject
Resultproject
RoleNameproject
Targetproject
TimeGeneratedproject
InitiatorNameextend
InitiatorUPNSuffixextend
TargetNameextend
TargetUPNSuffixextend