Query Details

Intune Stale Device Compliant Ghost

Query

id: 5a6f1c3e-2a1b-4c9e-9f01-11a2b3c4d515
name: Intune - Stale managed device still reporting as compliant
description: |
  Devices that have not checked into Intune for 30+ days but are still returning a `Compliant`
  posture in IntuneDeviceComplianceOrg. Attackers abuse this condition when they copy or replay
  a device's PRT / compliance token from a dormant endpoint — the real device is offline but
  compliance-gated Conditional Access still accepts the token. High FP potential; review with
  sign-in logs for the same DeviceId.
severity: Medium
requiredDataConnectors:
  - connectorId: AzureMonitor(IntuneLogs)
    dataTypes:
      - IntuneDevices
      - IntuneDeviceComplianceOrg
queryFrequency: 6h
queryPeriod: 14d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - CredentialAccess
  - DefenseEvasion
relevantTechniques:
  - T1552
  - T1550
query: |
  let stale =
      IntuneDevices
      | where TimeGenerated > ago(14d)
      | summarize LastSeen = max(todatetime(LastContact)) by DeviceId = tostring(DeviceId),
                                                             DeviceName = tostring(DeviceName),
                                                             UPN = tolower(tostring(UPN))
      | where LastSeen < ago(30d);
  let stillCompliant =
      IntuneDeviceComplianceOrg
      | where TimeGenerated > ago(1d)
      | where ComplianceState =~ "Compliant"
      | summarize arg_max(TimeGenerated, *) by DeviceId = tostring(DeviceId)
      | project DeviceId, ComplianceState, ReportedOn = TimeGenerated;
  stale
  | join kind=inner stillCompliant on DeviceId
  | project DeviceId, DeviceName, UPN, LastSeen, ReportedOn, ComplianceState
  | extend AccountCustomEntity = UPN, HostCustomEntity = DeviceName
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: AccountCustomEntity
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: HostCustomEntity
version: 1.0.0
kind: Scheduled

Explanation

This query is designed to identify devices that have not checked into Intune for over 30 days but are still marked as "Compliant" in the IntuneDeviceComplianceOrg records. This situation can be exploited by attackers who use a device's Primary Refresh Token (PRT) or compliance token from a device that is no longer active. Although the real device is offline, the compliance-gated Conditional Access still accepts the token, potentially allowing unauthorized access.

Here's a simplified breakdown of the query:

  1. Stale Devices Identification:

    • It looks at devices that have reported to Intune within the last 14 days.
    • It identifies devices that have not been in contact for over 30 days.
  2. Still Compliant Devices:

    • It checks for devices that have been marked as "Compliant" in the last day.
    • It gathers the most recent compliance state for each device.
  3. Join and Filter:

    • It combines the list of stale devices with those still marked as compliant.
    • The result is a list of devices that are both stale and still showing as compliant.
  4. Output:

    • The output includes the Device ID, Device Name, User Principal Name (UPN), last seen date, reported compliance date, and compliance state.
    • It also maps these to account and host entities for further analysis.
  5. Operational Details:

    • The query runs every 6 hours and looks back over a 14-day period.
    • It triggers an alert if any such devices are found.

The query is part of a scheduled task with a medium severity level, focusing on tactics related to Credential Access and Defense Evasion, specifically techniques T1552 and T1550. It suggests reviewing sign-in logs for the same DeviceId due to the high potential for false positives.

Details

David Alonso profile picture

David Alonso

Released: April 22, 2026

Tables

IntuneDevicesIntuneDeviceComplianceOrg

Keywords

DevicesIntuneComplianceTokenEndpointAccessLogs

Operators

let|where>ago()summarizemax()todatetime()bytostring()tolower()<=~arg_max()*projectjoinkind=innerextend

Actions