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:

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, PolicyIdThe 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:

// 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:

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)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 🤝.
Microsoft learn - Send Intune log data to Azure Storage, Event Hubs, or Log Analytics (https://learn.microsoft.com/en-us/intune/governance/integrate-azure-monitor) ↩︎
Microsoft learn - bag_unpack plugin (https://learn.microsoft.com/en-us/kusto/query/bag-unpack-plugin?view=azure-monitor) ↩︎
Microsoft learn - kusto - series_decompose_anomalies() (https://learn.microsoft.com/en-us/kusto/query/series-decompose-anomalies-function) ↩︎
Microsoft learn - Create or edit a log search alert rule (https://learn.microsoft.com/en-us/azure/azure-monitor/alerts/alerts-create-log-alert-rule#configure-alert-rule-conditions) ↩︎
