Skip to main content

Detecting Intune device compliance drift with KQL

Why are my Intune devices no longer compliant? Rolling out new compliance policies, raising minimum OS versions, or adjusting other controls can all cause devices to drift out of compliance. Ultimately, this impacts resource access whenever Conditional Access enforces a compliant device.

If you forward the IntuneOperationalLogs to a Log Analytics workspace1, you can query, parse, and alert on non-compliance events with just a few lines of KQL:

Managed Device Not Compliant
Anonymized example of noncompliant devices
IntuneOperationalLogs 
| where OperationName == "Compliance"
| extend Properties = parse_json(Properties)
| evaluate bag_unpack(Properties)
| where AlertType == @"Managed Device Not Compliant"
| extend UserPrincipalName = iif(UserDisplayName != "System account", strcat(UserName, '@', UPNSuffix), "System account")
// Extract the reason
| extend ReasonRaw = tostring(split(Description, '||')[0])
// Parse the compliance Policy ID
| parse ReasonRaw with ReasonParsed:string "_IID_" PolicyIdRaw:string
| extend Reason = coalesce(ReasonParsed, ReasonRaw)
| extend PolicyId = coalesce(PolicyIdRaw, 'DefaultDeviceCompliancePolicy')
| project-away *Raw
| project-reorder TimeGenerated, UserPrincipalName, DeviceHostName, IntuneDeviceId ,Reason, PolicyId
Note

The Properties column is a serialized JSON string that holds all the non-compliance details. The bag_unpack plugin2 expands every property in the bag into its own column, which keeps the rest of the query much simpler.

Or summarize the device count over a rolling window to spot trends per reason and policy:

Summarized reason
… Summarized by Device Count
// Prepend the above query
| summarize count() by Reason, PolicyId, bin(TimeGenerated, 7d)

We can take this one step further with the series_decompose_anomalies()3 KQL function to catch spikes of non-compliant devices early, for example right after a policy change or as part of ongoing operational monitoring. The query below builds a 90-day baseline at a 1 hour resolution and flags any bucket with an anomaly score above 5 from the baseline:

Detect non-compliant anomalies
Anomaly detection for non-compliant devices
let Interval = 1h;
let LookBack = 90d;
let AnomalyThreshold = 5;
let RuleWindow = 1h;
IntuneOperationalLogs 
| where TimeGenerated > ago(LookBack)
| where OperationName == "Compliance"
| extend Properties = parse_json(Properties)
| evaluate bag_unpack(Properties)
| where AlertType == @"Managed Device Not Compliant"
| make-series DeviceCount = dcount(IntuneDeviceId) on TimeGenerated from ago(LookBack) to now() step Interval 
| extend (AnomalyDetected, AnomalyScore, Baseline) = series_decompose_anomalies(DeviceCount, AnomalyThreshold, -1, 'linefit')
| mv-expand
    DeviceCount to typeof(double),
    TimeGenerated to typeof(datetime),
    AnomalyDetected to typeof(bool),
    AnomalyScore to typeof(double),
    Baseline to typeof(long)
| where AnomalyDetected
| where TimeGenerated > ago(2*RuleWindow)
Tip

Wire the last query up to a scheduled Log Analytics alert and you’ll get notified when compliance starts drifting. Note that alert rule limitations4 require slight modifications to the query.

Kudos to Janic for the inspiration and the pointer to the IntuneOperationalLogs table 🤝.

Nicola Suter
Author
Nicola Suter
Building cyber defense with the latest Microsoft technology available today - to defeat tomorrows threats