Remote Session Anomaly Detection with the Series Decompose Anomalies operator
By Gianni Castaldi
One of my favorite KQL functions is the function series_decompose_anomalies. I use it for all my funky KQLs where I want to do anomaly detections.
The KQL which I’m explaining today does an anomaly detection from the Microsoft Threat Protection portal, on all the logon events with the type “Remote Interactive”, this way it shows all the new incoming remote connections in example Remote Desktop.
The query consists of 5 steps
- Get all the device logon events with the type “Remote Interactive”
- Make a series of distinct devices in the lookback period based on the timeframe and per user account
- Use the function “series_decompose_anomalies” to do anomaly detection on the series.
- Use mv-expand to make it possible to put all items on a single row
- Use a filter to only display anomalies, where the distinct devices are greater than the threshold and where the timestamp is in the latest timeframe
// How long to lookback
let lookBack_long = 30d;
// Timeframe for the series
let TimeFrame = 3h;
// Anomaly threshold
let AnomalyThreshold = 3;
// Distinct Device Threshold
let DeviceThreshold = 4;
DeviceLogonEvents
// Look for all events with the type Remote Interactive
| where LogonType in ("RemoteInteractive")
// Make a series based on Distinct devices by User Accounts
| make-series DistinctDeviceCount = dcount(DeviceId), ReportId = max(ReportId) on Timestamp in range(startofday(ago(lookBack_long)),now(), TimeFrame) by AccountName, AccountSid
// Do anomaly detection on DistinctDeviceCount
| extend (AnomaliesDetected, AnomaliesScore, AnomaliesBaseline) = series_decompose_anomalies(DistinctDeviceCount, AnomalyThreshold, -1, 'linefit')
// Place all the items on a single line
| mv-expand DistinctDeviceCount to typeof(double), Timestamp to typeof(datetime), ReportId to typeof(double), AnomaliesDetected to typeof(double), AnomaliesScore to typeof(double), AnomaliesBaseline to typeof(long)
// Show all rows with a detected anomaly and where the threshold is higher than DeviceThreshold
| where AnomaliesDetected == 1 and DistinctDeviceCount >= DeviceThreshold
// Only show alerts in the TimeFrame
| where Timestamp >= ago(TimeFrame)