Detection rules › Kusto

Privileged Accounts - Sign in Failure Spikes

Status
available
Severity
high
Time window
14d
Group by
AccountUPN, 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 Access

Telemetry coverage

Rule body

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 and let TimeSeriesAlerts are inlined into the numbered stages below.

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)
};

Stage 1: source

let aadSignin

Stage 2: source

let aadNonInt

Stage 3: source

let allSignins

Stage 4: source

let TimeSeriesAlerts

Stage 5: union

union of 2 branches

Stage 6: source

IdentityInfo

Stage 7: where

where TimeGenerated > ago(1209600s)

Stage 8: summarize

summarize by AccountUPN

Stage 9: mv-expand

mv-expand AssignedRoles

Stage 10: where

where (AssignedRoles contains "Admin" or GroupMembership contains "Admin")

Stage 11: summarize

summarize AccountUPN, Roles by AccountUPN

Stage 12: join

join kind=inner (...) on AccountUPN, UserPrincipalName

Stage 13: extend

extend AccountCustomEntity, Roles, timestamp

Stage 14: source

IdentityInfo

Stage 15: where

where TimeGenerated > ago(1209600s)

Stage 16: summarize

summarize by AccountUPN

Stage 17: mv-expand

mv-expand AssignedRoles

Stage 18: where

where (AssignedRoles contains "Admin" or GroupMembership contains "Admin")

Stage 19: summarize

summarize AccountUPN, Roles by AccountUPN

Stage 20: join

join kind=inner (...) on AccountUPN, UserPrincipalName

Stage 21: extend

extend AccountCustomEntity, Roles, timestamp

Stage 22: summarize

summarize HourlyCount by UserPrincipalName, Roles

Stage 23: extend

extend anomalies, baseline, score

Stage 24: mv-expand

mv-expand HourlyCount

Stage 25: where

where anomalies > 0 and baseline > 5

Stage 26: extend

extend AnomalyHour

Stage 27: project

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

Stage 28: where

where TimeGenerated > ago(86400s)

Stage 29: join

join kind=inner (allSignins) on UserPrincipalName, AnomalyHour, DateHour

Stage 30: project

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

Stage 31: extend

extend Name, UPNSuffix

Indicators

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNameproject
AuthenticationRequirementproject
Categoryproject
ClientAppUsedproject
ConditionalAccessStatusproject
IPAddressproject
LatestAnomalyTimeproject
Locationproject
OperationNameproject
PartialFailedSigninsproject
ResourceDisplayNameproject
ResultDescriptionproject
ResultTypeproject
Rolesproject
TotalFailedSigninsproject
UserAgentproject
UserDisplayNameproject
UserPrincipalNameproject
anomaliesproject
baselineproject
scoreproject
Nameextend
UPNSuffixextend