Detection rules › Kusto

Privileged Accounts - Sign in Failure Spikes

Status
available
Severity
high
Time window
14d
Group by
AnomalyHour, DateHour, Roles, UserPrincipalName
Source
github.com/Azure/Azure-Sentinel

' Identifies spike in failed sign-ins from Privileged accounts. Privileged accounts list can be based on IdentityInfo UEBA table. Spike is determined based on Time series anomaly which will look at historical baseline values. Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor'

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078.004 Valid Accounts: Cloud Accounts

Rule body kusto

id: 34c5aff9-a8c2-4601-9654-c7e46342d03b
name: Privileged Accounts - Sign in Failure Spikes
description: |
  ' Identifies spike in failed sign-ins from Privileged accounts. Privileged accounts list can be based on IdentityInfo UEBA table.
  Spike is determined based on Time series anomaly which will look at historical baseline values.
  Ref : https://docs.microsoft.com/azure/active-directory/fundamentals/security-operations-privileged-accounts#things-to-monitor'
severity: High
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
      - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: BehaviorAnalytics
    dataTypes:
      - IdentityInfo
queryFrequency: 1d
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - InitialAccess
relevantTechniques:
  - T1078.004
tags:
  - AADSecOpsGuide
query: |
  let starttime = 14d;
  let timeframe = 1d;
  let scorethreshold = 3;
  let baselinethreshold = 5;
  let aadFunc = (tableName:string){
      IdentityInfo
      | where TimeGenerated > ago(starttime)
      | summarize arg_max(TimeGenerated, *) by AccountUPN
      | mv-expand AssignedRoles
      | where AssignedRoles contains 'Admin' or GroupMembership has "Admin"
      | summarize Roles = make_list(AssignedRoles) by AccountUPN = tolower(AccountUPN)
      | join kind=inner (
          table(tableName)
          | where TimeGenerated between (startofday(ago(starttime))..startofday(now()))
          | where ResultType != 0
          | extend UserPrincipalName = tolower(UserPrincipalName)
      ) on $left.AccountUPN == $right.UserPrincipalName
      | extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName, Roles = tostring(Roles)
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  let allSignins = union isfuzzy=true aadSignin, aadNonInt;
  let TimeSeriesAlerts = 
      allSignins
      | make-series HourlyCount=count() on TimeGenerated from startofday(ago(starttime)) to startofday(now()) step 1h by UserPrincipalName, Roles
      | extend (anomalies, score, baseline) = series_decompose_anomalies(HourlyCount, scorethreshold, -1, 'linefit')
      | mv-expand HourlyCount to typeof(double), TimeGenerated to typeof(datetime), anomalies to typeof(double), score to typeof(double), baseline to typeof(long)
      // Filtering low count events per baselinethreshold
      | where anomalies > 0 and baseline > baselinethreshold
      | extend AnomalyHour = TimeGenerated
      | project UserPrincipalName, Roles, AnomalyHour, TimeGenerated, HourlyCount, baseline, anomalies, score;
  // Filter the alerts for specified timeframe
  TimeSeriesAlerts
  | where TimeGenerated > startofday(ago(timeframe))
  | join kind=inner ( 
      allSignins
      | where TimeGenerated > startofday(ago(timeframe))
      // create a new column and round to hour
      | extend DateHour = bin(TimeGenerated, 1h)
      | summarize PartialFailedSignins = count(), LatestAnomalyTime = arg_max(TimeGenerated, *) by bin(TimeGenerated, 1h), OperationName, Category, ResultType, ResultDescription, UserPrincipalName, Roles, UserDisplayName, AppDisplayName, ClientAppUsed, IPAddress, ResourceDisplayName
  ) on UserPrincipalName, $left.AnomalyHour == $right.DateHour
  | project LatestAnomalyTime, OperationName, Category, UserPrincipalName, Roles = todynamic(Roles), UserDisplayName, ResultType, ResultDescription, AppDisplayName, ClientAppUsed, UserAgent, IPAddress, Location, AuthenticationRequirement, ConditionalAccessStatus, ResourceDisplayName, PartialFailedSignins, TotalFailedSignins = HourlyCount, baseline, anomalies, score
  | extend Name = tostring(split(UserPrincipalName,'@',0)[0]), UPNSuffix = tostring(split(UserPrincipalName,'@',1)[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserPrincipalName    
      - identifier: Name
        columnName: Name      
      - identifier: UPNSuffix
        columnName: UPNSuffix
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: IPAddress
version: 1.1.1
kind: Scheduled

Stages and Predicates

Parameters

let starttime = 14d;
let timeframe = 1d;
let scorethreshold = 3;
let baselinethreshold = 5;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let allSignins = union isfuzzy=true aadSignin, aadNonInt;

Let binding: aadFunc

let aadFunc = (tableName:string){
    IdentityInfo
    | where TimeGenerated > ago(starttime)
    | summarize arg_max(TimeGenerated, *) by AccountUPN
    | mv-expand AssignedRoles
    | where AssignedRoles contains 'Admin' or GroupMembership has "Admin"
    | summarize Roles = make_list(AssignedRoles) by AccountUPN = tolower(AccountUPN)
    | join kind=inner (
        table(tableName)
        | where TimeGenerated between (startofday(ago(starttime))..startofday(now()))
        | where ResultType != 0
        | extend UserPrincipalName = tolower(UserPrincipalName)
    ) on $left.AccountUPN == $right.UserPrincipalName
    | extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName, Roles = tostring(Roles)
};

Derived from starttime.

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

Stage 1: union

union of 2 branches

Stage 2: source

aadFunc

Stage 3: source

aadFunc

Stage 4: summarize

summarize HourlyCount by UserPrincipalName, Roles

Stage 5: extend

extend anomalies, baseline, score

Stage 6: mv-expand

mv-expand HourlyCount

Stage 7: where

where anomalies > 0 and baseline > 5

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

Stage 8: extend

extend AnomalyHour

Stage 9: project

project AnomalyHour, HourlyCount, Roles, TimeGenerated, UserPrincipalName, anomalies, baseline, score

Stage 10: where

where ...

Stage 11: join

join kind=inner (...)

Stage 12: project

project AppDisplayName, AuthenticationRequirement, Category, ClientAppUsed, ConditionalAccessStatus, IPAddress, LatestAnomalyTime, Location, OperationName, PartialFailedSignins, ResourceDisplayName, ResultDescription, ResultType, Roles, TotalFailedSignins, UserAgent, UserDisplayName, UserPrincipalName, anomalies, baseline, score

Stage 13: extend

extend Name, UPNSuffix

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
anomaliesgt
  • 0 transforms: cased
baselinegt
  • 5 transforms: cased

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
AppDisplayNameproject
AuthenticationRequirementproject
Categoryproject
ClientAppUsedproject
ConditionalAccessStatusproject
IPAddressproject
LatestAnomalyTimeproject
Locationproject
OperationNameproject
PartialFailedSigninsproject
ResourceDisplayNameproject
ResultDescriptionproject
ResultTypeproject
Rolesproject
TotalFailedSigninsproject
UserAgentproject
UserDisplayNameproject
UserPrincipalNameproject
anomaliesproject
baselineproject
scoreproject
Nameextend
UPNSuffixextend