I manged (ages ago) to get the Country to show, but never looked past that or fully tested it
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, countryName=split(locationString,"/").[0]
// Create time series
| make-series dLocationCount = dcount(locationString)
on TimeGenerated
in range(startofday(ago(lookBack_long)), now(), 1d)
by UserPrincipalName, AppDisplayName, tostring(countryName)
// 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, countryName=split(locationString,"/").[0]
| make-series dLocationCount = dcount(locationString)
on TimeGenerated
in range(startofday(ago(lookBack_med)), now(), 1d)
by UserPrincipalName, AppDisplayName, tostring(countryName)
| 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, countryName=split(locationString,"/").[0]
| make-series dLocationCount = dcount(locationString)
on TimeGenerated
in range(startofday(ago(lookBack)), now(), 1d)
by UserPrincipalName, AppDisplayName, tostring(countryName)
| 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
Posting in case it's useful - please Accept the answer if it helps