Hello,
Can someone please explain this scheduled analytics rule logic, thanks in advance!
Name
Anomalous sign-in location by user account and authenticating application
Description
This query over Azure Active Directory sign-in considers all user sign-ins for each Azure Active Directory application and picks out the most anomalous change in location profile for a user within an individual application.
An alert is generated for recent sign-ins that have location counts that are anomalous over last day but also over the last 3-day and 7-day periods.
Please note that on workspaces with larger volume of Sign in data (~10M+ events a day) may timeout when using this default query time period. It is recommended that you test and tune this appropriately for the workspace.
Rule frequency
Run query every 1 day
Rule period
Last 14 days data
RULE LOGIC:
let lookBack_long = 7d;
let lookBack_med = 3d;
let lookBack = 1d;
let aadFunc = (tableName:string){
table(tableName)
| where TimeGenerated >= startofday(ago(lookBack_long))
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
| extend locationString = strcat(tostring(LocationDetails.countryOrRegion), "/", tostring(LocationDetails.state), "/", tostring(LocationDetails.city), ";")
| project TimeGenerated, AppDisplayName , UserPrincipalName, locationString
// Create time series
| make-series dLocationCount = dcount(locationString) on TimeGenerated in range(startofday(ago(lookBack_long)),now(), 1d)
by UserPrincipalName, AppDisplayName
// Compute best fit line for each entry
| extend (RSquare,Slope,Variance,RVariance,Interception,LineFit)=series_fit_line(dLocationCount)
// Chart the 3 most interesting lines
// A 0-value slope corresponds to an account being completely stable over time for a given Azure Active Directory application
| where Slope > 0.3
| top 50 by Slope desc
| join kind = leftsemi (
table(tableName)
| where TimeGenerated >= startofday(ago(lookBack_med))
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
| extend locationString = strcat(tostring(LocationDetails.countryOrRegion), "/", tostring(LocationDetails.state), "/", tostring(LocationDetails.city), ";")
| project TimeGenerated, AppDisplayName , UserPrincipalName, locationString
| make-series dLocationCount = dcount(locationString) on TimeGenerated in range(startofday(ago(lookBack_med)) ,now(), 1d)
by UserPrincipalName, AppDisplayName
| extend (RSquare,Slope,Variance,RVariance,Interception,LineFit)=series_fit_line(dLocationCount)
| where Slope > 0.3
| top 50 by Slope desc
) on UserPrincipalName, AppDisplayName
| join kind = leftsemi (
table(tableName)
| where TimeGenerated >= startofday(ago(lookBack))
| extend DeviceDetail = todynamic(DeviceDetail), Status = todynamic(DeviceDetail), LocationDetails = todynamic(LocationDetails)
| extend locationString = strcat(tostring(LocationDetails.countryOrRegion), "/", tostring(LocationDetails.state), "/", tostring(LocationDetails.city), ";")
| project TimeGenerated, AppDisplayName , UserPrincipalName, locationString
| make-series dLocationCount = dcount(locationString) on TimeGenerated in range(startofday(ago(lookBack)) ,now(), 1d)
by UserPrincipalName, AppDisplayName
| extend (RSquare,Slope,Variance,RVariance,Interception,LineFit)=series_fit_line(dLocationCount)
| where Slope > 5
| top 50 by Slope desc
// Higher threshold requirement on last day anomaly
) on UserPrincipalName, AppDisplayName
| extend timestamp = TimeGenerated, AccountCustomEntity = UserPrincipalName
};
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
union isfuzzy=true aadSignin, aadNonInt