~/2025/10/22/powershell-intune-force-a-compliance-policy-re-evaluation-fleet-wide.md

PowerShell: Intune – Force Compliance Re-Evaluation Fleet-Wide

---
author: 
date: 
read: 3 min
in:   [ps, scripts]
tags: [powershell, intune]
---

$ grep -n '^#' post.md

After pushing a new or corrected compliance policy, the last thing I want is to wait out each device's normal check-in, which Microsoft estimates at about every 8 hours. Clicking Sync one device at a time in the Intune console doesn't scale past a handful of machines, and the console's bulk device actions handle up to 100 devices at a time. The same action is exposed through Microsoft Graph as syncDevice on a managed device (POST /deviceManagement/managedDevices/{id}/syncDevice, v1.0), and the Graph PowerShell SDK wraps it as Sync-MgDeviceManagementManagedDevice. This script pulls every managed device matching an OS and name filter, optionally only the ones not currently compliant, sends a sync to each with a delay between calls, and writes a CSV of what happened.

Requirements

  • PowerShell 7.x recommended (Windows PowerShell 5.1 works too).
  • Microsoft.Graph.Authentication and Microsoft.Graph.DeviceManagement modules (Install-Module Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement -Scope CurrentUser).
  • Two delegated Graph permissions (the script signs in interactively with Connect-MgGraph -Scopes; running it app-only would need a certificate or managed-identity sign-in and the same two as application permissions):
    • DeviceManagementManagedDevices.PrivilegedOperations.All: the only permission syncDevice accepts.
    • DeviceManagementManagedDevices.Read.All: needed to list devices with Get-MgDeviceManagementManagedDevice. The privileged-operations permission doesn't include read access, and the first version of this script only requested that one, so the device query failed.
  • An Intune role for the signed-in identity that allows the remote sync task (Intune Administrator, or a custom role with that remote task).
  • An active Intune license on the tenant (a stated requirement of the Intune Graph API).
  • Devices need to be online and reachable through their platform's push service to act on the request immediately. An offline device picks it up the next time it checks in.

Parameters

NameTypeRequiredDescription
-OperatingSystemStringNoManaged device operatingSystem value to target. Defaults to Windows.
-DeviceNamePatternStringNoWildcard pattern matched against the device name. Defaults to * (every device).
-OnlyNonCompliantSwitchNoSkips devices whose complianceState is already compliant.
-ThrottleMsIntNoDelay in milliseconds between sync calls. Defaults to 500.
-ReportPathStringNoPath to the CSV report written after the run. Defaults to a timestamped file in the current directory.

Usage

Preview which devices would be synced without calling the action:

powershell
.\Invoke-FleetComplianceResync.ps1 -WhatIf

After fixing a compliance policy, resync only the laptops that are currently failing it:

powershell
.\Invoke-FleetComplianceResync.ps1 -DeviceNamePattern "LAPTOP-*" -OnlyNonCompliant -ThrottleMs 750

Sample output:

text
Fetching managed devices (OS: Windows, name pattern: LAPTOP-*)...
214 device(s) matched, 37 after the compliance filter.
Sync requested for 37 device(s), 1 failed. Report: .\fleet-sync-report-20251022-090512.csv

Look at failures afterwards:

powershell
Import-Csv .\fleet-sync-report-20251022-090512.csv | Where-Object { $_.Status -eq "Failed" } | Format-Table DeviceName, Error

Confirm the devices actually checked in. LastSyncDateTime in the report is the value from before the sync, so compare it with a fresh read an hour later:

powershell
$before = Import-Csv .\fleet-sync-report-20251022-090512.csv | Where-Object { $_.Status -eq "Synced" }
foreach ($row in $before) {
    $now = Get-MgDeviceManagementManagedDevice -ManagedDeviceId $row.DeviceId -Property "deviceName,lastSyncDateTime,complianceState"
    [PSCustomObject]@{
        DeviceName      = $now.DeviceName
        SyncedBefore    = $row.LastSyncDateTime
        SyncedNow       = $now.LastSyncDateTime
        ComplianceState = $now.ComplianceState
    }
}

Script

powershell
<#
.SYNOPSIS
    Sends the Intune syncDevice action to every matching managed device so
    compliance and configuration policy are re-evaluated immediately.
.DESCRIPTION
    Connects to Microsoft Graph, lists managed devices that match the operating
    system and device name filters (optionally only those not compliant), and
    calls Sync-MgDeviceManagementManagedDevice (the v1.0 syncDevice action) on
    each one. The sync makes the device check in with Intune now instead of at
    its next scheduled check-in. Calls are spaced by -ThrottleMs, a throttled
    call (HTTP 429) is retried once after a back-off, and the result of each
    call is written to a CSV report. Supports -WhatIf.
.PARAMETER OperatingSystem
    Managed device operatingSystem value to target. Defaults to Windows.
.PARAMETER DeviceNamePattern
    Wildcard pattern matched against the device name. Defaults to * (every device).
.PARAMETER OnlyNonCompliant
    Skips devices whose complianceState is already compliant.
.PARAMETER ThrottleMs
    Delay in milliseconds between sync calls. Defaults to 500.
.PARAMETER ReportPath
    Path to the CSV report written after the run.
.EXAMPLE
    .\Invoke-FleetComplianceResync.ps1 -WhatIf
.EXAMPLE
    .\Invoke-FleetComplianceResync.ps1 -DeviceNamePattern "LAPTOP-*" -OnlyNonCompliant -ThrottleMs 750
.NOTES
    Author  : Thomas Lasswell (https://www.techcolumnist.com)
    Version : 1.0 (2025-10-22)
    Requires: Microsoft.Graph.Authentication, Microsoft.Graph.DeviceManagement;
              DeviceManagementManagedDevices.Read.All and
              DeviceManagementManagedDevices.PrivilegedOperations.All.
.LINK
    https://learn.microsoft.com/en-us/graph/api/intune-devices-manageddevice-syncdevice?view=graph-rest-1.0
#>

[CmdletBinding(SupportsShouldProcess = $true)]
param (
    [Parameter()]
    [string]$OperatingSystem = "Windows",

    [Parameter()]
    [string]$DeviceNamePattern = "*",

    [Parameter()]
    [switch]$OnlyNonCompliant,

    [Parameter()]
    [int]$ThrottleMs = 500,

    [Parameter()]
    [string]$ReportPath = ".\fleet-sync-report-$(Get-Date -Format 'yyyyMMdd-HHmmss').csv"
)

Import-Module Microsoft.Graph.Authentication
Import-Module Microsoft.Graph.DeviceManagement

Connect-MgGraph -Scopes "DeviceManagementManagedDevices.Read.All", "DeviceManagementManagedDevices.PrivilegedOperations.All" -NoWelcome

Write-Host "Fetching managed devices (OS: $OperatingSystem, name pattern: $DeviceNamePattern)..."

$escapedOs = $OperatingSystem.Replace("'", "''")
$devices = @(Get-MgDeviceManagementManagedDevice -All -Filter "operatingSystem eq '$escapedOs'" -Property "id,deviceName,complianceState,lastSyncDateTime,operatingSystem" |
    Where-Object { $_.DeviceName -like $DeviceNamePattern })
$matchedCount = $devices.Count

if ($OnlyNonCompliant) {
    $devices = @($devices | Where-Object { $_.ComplianceState -ne "compliant" })
}

Write-Host "$matchedCount device(s) matched, $($devices.Count) after the compliance filter."

$results = foreach ($device in $devices) {
    $status = "Skipped"
    $errorMessage = $null

    if ($PSCmdlet.ShouldProcess($device.DeviceName, "Sync-MgDeviceManagementManagedDevice")) {
        $attempt = 0
        while ($status -ne "Synced" -and $attempt -lt 2) {
            $attempt++
            try {
                Sync-MgDeviceManagementManagedDevice -ManagedDeviceId $device.Id -ErrorAction Stop
                $status = "Synced"
                $errorMessage = $null
            } catch {
                $status = "Failed"
                $errorMessage = $_.Exception.Message
                if ($errorMessage -match "TooManyRequests|429" -and $attempt -lt 2) {
                    # Back off once when Graph throttles the call, then retry.
                    Start-Sleep -Seconds 30
                } else {
                    break
                }
            }
        }

        Start-Sleep -Milliseconds $ThrottleMs
    }

    [PSCustomObject]@{
        DeviceName       = $device.DeviceName
        DeviceId         = $device.Id
        ComplianceState  = $device.ComplianceState
        LastSyncDateTime = $device.LastSyncDateTime
        Status           = $status
        Error            = $errorMessage
    }
}

$results | Export-Csv -Path $ReportPath -NoTypeInformation -Encoding utf8

$failed = @($results | Where-Object { $_.Status -eq "Failed" })
Write-Host "Sync requested for $(@($results).Count) device(s), $($failed.Count) failed. Report: $ReportPath"

Notes

  • A sync makes the device check in now, and compliance is evaluated at check-in. It doesn't change a device's compliance state by itself: a device that genuinely fails a policy still shows non-compliant after the sync, just sooner than it otherwise would.
  • A successful call only means Intune accepted the request (syncDevice returns 204 No Content). Whether the device checked in shows up later in lastSyncDateTime, which is why the Usage section compares before and after.
  • Throttling: syncDevice is a write against the Intune devices service, which Microsoft documents at 200 POST/PUT/PATCH/DELETE requests per 20 seconds per app per tenant and 400 per tenant across all apps. The default 500 ms spacing works out to 40 calls per 20 seconds, well under that, and leaves room for other tools calling Graph at the same time. If the report shows throttling errors, raise -ThrottleMs before assuming anything is wrong with those devices. The script's own single 429 retry is only a backstop: the Graph PowerShell SDK already retries throttled requests itself, honoring Retry-After (Microsoft: the SDKs "already implement handlers that rely on the Retry-After header or default to an exponential backoff retry policy"), up to MaxRetry times, 3 by default, which Set-MgRequestContext can change. A 429 only reaches the script once those retries are used up.
  • -Property keeps the device query small. Get-MgDeviceManagementManagedDevice otherwise returns every property of every device, which gets slow on a large tenant.
  • This is for enrolled devices that still communicate. A device that hasn't checked in for weeks needs a different conversation than a resync: look at its last check-in date and your device cleanup rules first.

Source