User Accounts - Successful Sign in Spikes
Description
' Identifies measureable increase in successful sign-ins from user accounts. 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-user-accounts#monitoring-for-successful-unusual-sign-ins'
Query · kql
let starttime = 14d;
let timeframe = 1d;
let scorethreshold = 5;
let baselinethreshold = 25;
let aadFunc = (tableName:string){
// Succesful signins.
table(tableName)
| where TimeGenerated between (startofday(ago(starttime))..startofday(ago(timeframe)))
| where ResultType == 0
| extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
let allSignins = union isfuzzy=true aadSignin, aadNonInt ;
let TimeSeriesData = union isfuzzy=true aadSignin, aadNonInt
| project TimeGenerated, UserPrincipalName
| make-series HourlyCount=count() on TimeGenerated from startofday(ago(starttime)) to startofday(now()) step timeframe by UserPrincipalName
| project TimeGenerated, UserPrincipalName, HourlyCount;
let TimeSeriesAlerts = TimeSeriesData
| 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)
| where anomalies > 0 | extend AnomalyHour = TimeGenerated
| where baseline > baselinethreshold // Filtering low count events per baselinethreshold
| project UserPrincipalName, AnomalyHour, TimeGenerated, HourlyCount, baseline, anomalies, score;
let AnomalyHours = TimeSeriesAlerts | where TimeGenerated > ago(2d) | project TimeGenerated;
// Filter the alerts for specified timeframe
TimeSeriesAlerts
| where TimeGenerated > ago(2d)
| join kind=inner (
union isfuzzy=true aadSignin, aadNonInt
| where TimeGenerated > ago(2d)
| extend DateHour = bin(TimeGenerated, 1h) // create a new column and round to hour
| where DateHour in ((AnomalyHours)) //filter the dataset to only selected anomaly hours
| summarize HourlyCount=count(), LatestAnomalyTime = arg_max(timestamp,*) by bin(TimeGenerated,1h), OperationName, Category, ResultType, ResultDescription, UserPrincipalName, UserDisplayName, AppDisplayName, ClientAppUsed, IPAddress, ResourceDisplayName
) on UserPrincipalName
| project LatestAnomalyTime, OperationName, Category, UserPrincipalName, UserDisplayName, ResultType, ResultDescription, AppDisplayName, ClientAppUsed, UserAgent, IPAddress, Location, AuthenticationRequirement, ConditionalAccessStatus, ResourceDisplayName, HourlyCount, baseline, anomalies, score
| extend timestamp = LatestAnomalyTime, IPCustomEntity = IPAddress, AccountCustomEntity = UserPrincipalName