Emergency server help: get in touch

Intune Device Compliance Report: PowerShell Script via Graph

Free PowerShell script that exports Intune managed devices with compliance state, OS, last sync, user and encryption from Microsoft Graph to CSV or HTML.

Version
1.0.0
Last updated
October 6, 2026
Language
PowerShell
Tested on
Syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25 on 6 Oct 2026; not yet run against a live tenant/server.
License
MIT
Pricing
Free

Short answer: run .\Get-IntuneComplianceReport.ps1 -NonCompliantOnly -CsvPath .\noncompliant.csv. It reads every Intune managed device from Microsoft Graph (GET /deviceManagement/managedDevices) and exports the compliance state, OS and version, last sync time, primary user, ownership and encryption state. It needs the DeviceManagementManagedDevices.Read.All permission and an Intune role that can read devices. Add -NotSyncedInDays 30 to find devices that have stopped checking in.

Commands checked against the official documentation (linked below) on 6 October 2026; not yet run on our lab servers. The script was syntax-checked with PowerShell 7.4.6 and PSScriptAnalyzer 1.25 (no errors or warnings) but has not yet been run against a live Microsoft 365 tenant. Every cmdlet, endpoint, property and permission it uses was checked against Microsoft Learn. If something behaves differently for you, tell us and we will fix the script.

What it does

The Intune admin center shows compliance per device, but exporting it, filtering by OS and combining it with “last check-in” is clumsy, and the numbers are what auditors and Conditional Access care about. The script gives you one row per managed device with the fields you actually filter on:

  • Compliance: complianceState (compliant, noncompliant, inGracePeriod, conflict, error, unknown, configManager) and the grace period expiry.
  • Health: last sync time and days since sync, encryption, jailbreak state.
  • Who and what: primary user UPN and name, OS and version, manufacturer, model, serial number, ownership (company or personal), enrollment type and management agent.
  • IDs: Intune device ID and Entra device ID, so you can join the CSV with other reports.
  • Filters by state, OS (wildcards), staleness and user, applied locally so the script does not depend on which $filter operators the endpoint accepts.
  • Read-only. It does not sync, retire or wipe anything.

Requirements

  • Windows PowerShell 5.1 or PowerShell 7 with the Microsoft.Graph.Authentication module.
  • An active Intune licence in the tenant; Microsoft states the Graph API for Intune requires one.
  • Graph permission DeviceManagementManagedDevices.Read.All (delegated for interactive use, application for scheduled runs). It is the least privileged permission Microsoft lists for listing managed devices.
  • For interactive use your account also needs an Intune role that can read managed devices, for example the built-in Read Only Operator, or the Intune Administrator Entra role.

Download and first run

  1. Copy the script from the box on this page (or use the download button) and save it as C:\Scripts\Get-IntuneComplianceReport.ps1.
  2. If you downloaded the file, clear the “downloaded from the internet” mark so the execution policy lets it run: Unblock-File C:\Scripts\Get-IntuneComplianceReport.ps1.
  3. Install the module once, for your user: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser.
  4. Read the built-in help, then run it once interactively and look at the result on screen before you export or schedule anything.
cd C:\Scripts
Get-Help .\Get-IntuneComplianceReport.ps1 -Full
.\Get-IntuneComplianceReport.ps1
.\Get-IntuneComplianceReport.ps1 -NonCompliantOnly -OperatingSystem Windows

Options

ParameterWhat it doesDefault
-ComplianceStateOnly these states, for example noncompliant,inGracePeriod,error,conflictAll
-NonCompliantOnlyEverything that is not “compliant” (includes unknown and inGracePeriod)Off
-OperatingSystemOnly these OS names (Windows, iOS, Android, macOS…), wildcards allowedAll
-NotSyncedInDaysOnly devices whose last sync is at least this many days old (or never)Off
-UserPrincipalNameOnly devices whose primary user matches (wildcards; plain text = contains)All
-CsvPath / -HtmlPathWrite CSV and/or HTMLScreen only
-PassThruSend objects down the pipelineOff
-TenantId, -ClientId, -CertificateThumbprintApp-only sign-in for scheduled runsInteractive

Usage examples

# All devices to CSV
.\Get-IntuneComplianceReport.ps1 -CsvPath C:\Reports\intune-devices.csv

# Non-compliant Windows devices as an HTML page for the helpdesk
.\Get-IntuneComplianceReport.ps1 -NonCompliantOnly -OperatingSystem Windows -HtmlPath C:\Reports\noncompliant-windows.html

# Devices that stopped checking in a month ago (retire candidates)
.\Get-IntuneComplianceReport.ps1 -NotSyncedInDays 30 -CsvPath .\stale-devices.csv

# Compliance by OS, as counts
.\Get-IntuneComplianceReport.ps1 -PassThru | Group-Object OperatingSystem, ComplianceState | Sort-Object Name | Select-Object Count, Name

# Unencrypted company-owned devices
.\Get-IntuneComplianceReport.ps1 -PassThru | Where-Object { $_.OwnerType -eq 'company' -and $_.IsEncrypted -eq $false }

CSV columns

No sample output is shown because the script has not yet run against a live tenant. Columns (all from the managedDevice resource):

ColumnGraph property
DeviceNamedeviceName
UserPrincipalName, UserDisplayNameuserPrincipalName, userDisplayName (primary user)
OperatingSystem, OSVersionoperatingSystem, osVersion
ComplianceState, ComplianceGracePeriodExpirationcomplianceState, complianceGracePeriodExpirationDateTime
LastSyncUtc, DaysSinceSynclastSyncDateTime and whole days since then
IsEncrypted, JailBrokenisEncrypted, jailBroken
OwnerType, ManagementAgent, EnrollmentType, EnrolledUtcmanagedDeviceOwnerType, managementAgent, deviceEnrollmentType, enrolledDateTime
Manufacturer, Model, SerialNumbermanufacturer, model, serialNumber
AzureADDeviceId, IntuneDeviceIdazureADDeviceId, id

Schedule it

Scheduled runs cannot answer a sign-in prompt, so use app-only sign-in with a certificate. Microsoft’s steps are in Use app-only authentication with the Microsoft Graph PowerShell SDK; in short:

  1. Create an app registration in the Microsoft Entra admin center (single tenant).
  2. Create a certificate on the machine that will run the task, install it in the certificate store of the account that runs the task, and upload the public key (.cer) to the app registration.
  3. Under API permissions add these Application permissions for Microsoft Graph and grant admin consent: DeviceManagementManagedDevices.Read.All.
  4. Note the application (client) ID, the tenant ID and the certificate thumbprint.
$action = New-ScheduledTaskAction -Execute 'powershell.exe' `
    -Argument '-NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Get-IntuneComplianceReport.ps1 -NonCompliantOnly -CsvPath C:\Reports\noncompliant.csv -TenantId contoso.onmicrosoft.com -ClientId <app-id> -CertificateThumbprint <thumbprint>'
$trigger = New-ScheduledTaskTrigger -Weekly -DaysOfWeek Monday -At 7am
Register-ScheduledTask -TaskName 'Intune compliance report' -Action $action -Trigger $trigger -User 'CONTOSO\svc-reports' -Password '<password>'

Run the task as the account whose certificate store holds the certificate (a dedicated service account works well), and keep the private key on that machine only. The app has tenant-wide read access, so treat the certificate like an admin password and set an expiry date you will notice. The CSV is overwritten on every run; add the date to the file name in a small wrapper if you want history.

How it works

  1. Connect-MgGraph -Scopes DeviceManagementManagedDevices.Read.All, or app-only with the certificate.
  2. GET https://graph.microsoft.com/v1.0/deviceManagement/managedDevices, following @odata.nextLink until every device has been read. Throttling responses (429) and temporary 503/504 errors are retried.
  3. Each device becomes one row. Dates are converted to UTC; a sync date of 0001-01-01 is treated as “never”.
  4. The filters you passed are applied, the rows are sorted by state, OS and name, and the output is written.
  5. A summary line shows the count per compliance state and how many devices report as not encrypted.

Limitations

  • Which policy failed is not in this report. The managedDevice object only carries the overall state; per-policy and per-setting results are separate Graph resources and the Intune reports.
  • “unknown” usually means the device has not checked in since a policy was assigned, or no compliance policy targets it. Check your “Mark devices with no compliance policy assigned as” setting.
  • Co-managed devices may show configManager when compliance is evaluated by Configuration Manager.
  • Freshness: the state is what Intune last recorded; a device that has been offline for weeks reports its old state. Use DaysSinceSync.
  • Not yet run against a live tenant (see the note at the top).

Official documentation: List managedDevices · managedDevice resource type · Use app-only authentication with Graph PowerShell

Related: Intune Bulk Enrollment: Windows 11 Provisioning Package · Intune Win32 App Deployment: Reliable Packaging and Detection · Block USB Storage Group Policy: 5 Methods for Windows 11 · Autopilot Error 80180003 and 80180014: Fixes · Import ADMX templates (Office, Chrome, Edge) into Intune and the AD Central Store

See also: Microsoft 365 MFA Status Report: PowerShell Script for Graph · Entra ID Inactive Users Report: signInActivity PowerShell Script · Windows LAPS with Intune and Entra ID: Setup and Retrieval · Microsoft 365 User Offboarding PowerShell Script (with -WhatIf)

The script

Get-IntuneComplianceReport.ps1Download
# Intune Device Compliance Report: PowerShell Script via Graph (v1.0.0) - from srvScripts.com
# Source, docs and updates: https://srvscripts.com/scripts/intune-device-compliance-report/
# Copyright (c) 2026 srvScripts.com. MIT licence: if you copy, share or adapt this script, keep this notice and credit srvScripts.com.
<#
.SYNOPSIS
    Intune device compliance report from Microsoft Graph: compliance state, OS and version, last sync,
    primary user, ownership, encryption and hardware details for every managed device. CSV/HTML output.

.DESCRIPTION
    Read-only. Calls GET /deviceManagement/managedDevices (Microsoft Graph v1.0) through the
    Microsoft.Graph.Authentication module and follows @odata.nextLink until every device is read.
    Filters are applied locally so the script does not depend on which $filter operators the
    managedDevices endpoint accepts for each property.

    complianceState values (Microsoft Graph): unknown, compliant, noncompliant, conflict, error,
    inGracePeriod, configManager.

    Permissions:
      Delegated scope   DeviceManagementManagedDevices.Read.All
      Application       DeviceManagementManagedDevices.Read.All (admin consent)
      The signed-in admin also needs an Intune role that can read managed devices, for example the
      built-in Read Only Operator role, or the Intune Administrator Entra role.

.PARAMETER ComplianceState    Only these states (e.g. noncompliant,inGracePeriod,error,conflict).
.PARAMETER NonCompliantOnly   Shortcut: everything that is not compliant.
.PARAMETER OperatingSystem    Only these OS names (Windows, iOS, Android, macOS ...). Wildcards allowed.
.PARAMETER NotSyncedInDays    Only devices whose last successful sync is older than this many days.
.PARAMETER UserPrincipalName  Only devices whose primary user UPN matches (wildcards allowed).
.PARAMETER CsvPath            Write the result to this CSV file.
.PARAMETER HtmlPath           Write the result to this HTML file.
.PARAMETER PassThru           Output objects to the pipeline.
.PARAMETER TenantId           Tenant ID or domain (required for app-only sign-in).
.PARAMETER ClientId           App registration (client) ID for app-only sign-in.
.PARAMETER CertificateThumbprint  Certificate thumbprint for app-only sign-in.

.EXAMPLE  .\Get-IntuneComplianceReport.ps1 -CsvPath .\intune-devices.csv
.EXAMPLE  .\Get-IntuneComplianceReport.ps1 -NonCompliantOnly -OperatingSystem Windows -HtmlPath .\noncompliant-windows.html
.EXAMPLE  .\Get-IntuneComplianceReport.ps1 -NotSyncedInDays 30 -CsvPath .\stale-devices.csv
.EXAMPLE  .\Get-IntuneComplianceReport.ps1 -TenantId contoso.onmicrosoft.com -ClientId <app-id> -CertificateThumbprint <thumbprint> -CsvPath C:\Reports\intune.csv

.NOTES
    Name:     Get-IntuneComplianceReport.ps1
    Purpose:  Intune managed device compliance and health export
    Source:   https://srvscripts.com/scripts/intune-device-compliance-report/
    License:  MIT
    Version:  1.0.0
    Requires: Windows PowerShell 5.1 or PowerShell 7, module Microsoft.Graph.Authentication.
#>
[CmdletBinding(DefaultParameterSetName = 'Interactive')]
param(
    [ValidateSet('unknown', 'compliant', 'noncompliant', 'conflict', 'error', 'inGracePeriod', 'configManager')]
    [string[]]$ComplianceState,
    [switch]$NonCompliantOnly,
    [string[]]$OperatingSystem,
    [ValidateRange(1, 3650)]
    [int]$NotSyncedInDays,
    [string]$UserPrincipalName,
    [string]$CsvPath,
    [string]$HtmlPath,
    [switch]$PassThru,
    [Parameter(ParameterSetName = 'Interactive')]
    [Parameter(ParameterSetName = 'AppOnly', Mandatory = $true)]
    [string]$TenantId,
    [Parameter(ParameterSetName = 'AppOnly', Mandatory = $true)]
    [ValidatePattern('^[0-9a-fA-F-]{36}$')]
    [string]$ClientId,
    [Parameter(ParameterSetName = 'AppOnly', Mandatory = $true)]
    [ValidatePattern('^[0-9a-fA-F]{40}$')]
    [string]$CertificateThumbprint
)

Set-StrictMode -Version 2
$ErrorActionPreference = 'Stop'
$Graph = 'https://graph.microsoft.com/v1.0'

function Write-Status([string]$Message) { Write-Information $Message -InformationAction Continue }

function Invoke-GraphWithRetry([string]$Uri) {
    $attempt = 0
    while ($true) {
        $attempt++
        try { return Invoke-MgGraphRequest -Method GET -Uri $Uri -OutputType HashTable -ErrorAction Stop }
        catch {
            if ($attempt -lt 5 -and $_.Exception.Message -match '429|TooManyRequests|503|ServiceUnavailable|504|GatewayTimeout') {
                Start-Sleep -Seconds ([math]::Pow(2, $attempt) * 5); continue
            }
            throw
        }
    }
}

function Get-GraphCollection([string]$Uri) {
    $next = $Uri
    while ($next) {
        $r = Invoke-GraphWithRetry $next
        if ($r['value']) { foreach ($i in $r['value']) { $i } }
        $next = $r['@odata.nextLink']
    }
}

function Get-Value($Hash, [string]$Key) {
    if ($null -ne $Hash -and $Hash.ContainsKey($Key)) { return $Hash[$Key] }
    return $null
}

function ConvertTo-UtcDate($Value) {
    if ($null -eq $Value -or "$Value" -eq '') { return $null }
    if ($Value -is [datetime]) { $d = $Value.ToUniversalTime() }
    elseif ($Value -is [datetimeoffset]) { $d = $Value.UtcDateTime }
    else { $d = ([datetimeoffset]::Parse([string]$Value, [Globalization.CultureInfo]::InvariantCulture)).UtcDateTime }
    if ($d.Year -le 1) { return $null }   # 0001-01-01 means "never"
    return $d
}

# ---- Connect --------------------------------------------------------------------------------------------
if (-not (Get-Module -ListAvailable -Name Microsoft.Graph.Authentication)) {
    throw 'Module Microsoft.Graph.Authentication is not installed. Run: Install-Module Microsoft.Graph.Authentication -Scope CurrentUser'
}
Import-Module Microsoft.Graph.Authentication
if ($PSCmdlet.ParameterSetName -eq 'AppOnly') {
    Connect-MgGraph -TenantId $TenantId -ClientId $ClientId -CertificateThumbprint $CertificateThumbprint -NoWelcome
} else {
    $cp = @{ Scopes = @('DeviceManagementManagedDevices.Read.All'); NoWelcome = $true }
    if ($TenantId) { $cp.TenantId = $TenantId }
    Connect-MgGraph @cp
}
$ctx = Get-MgContext
if (-not $ctx) { throw 'Not connected to Microsoft Graph.' }

# ---- Read devices ---------------------------------------------------------------------------------------
Write-Status 'Reading Intune managed devices...'
try {
    $devices = @(Get-GraphCollection "$Graph/deviceManagement/managedDevices")
} catch {
    if ($_.Exception.Message -match 'Forbidden|403|Unauthorized|401') {
        throw "Graph refused managedDevices: $($_.Exception.Message). Check DeviceManagementManagedDevices.Read.All consent and that your account has an Intune role (for example Read Only Operator)."
    }
    throw
}
Write-Status ("{0} managed device(s) read." -f $devices.Count)

$now = (Get-Date).ToUniversalTime()
$rows = [System.Collections.Generic.List[object]]::new()
foreach ($d in $devices) {
    $sync = ConvertTo-UtcDate (Get-Value $d 'lastSyncDateTime')
    $rows.Add([pscustomobject][ordered]@{
        DeviceName                      = Get-Value $d 'deviceName'
        UserPrincipalName               = Get-Value $d 'userPrincipalName'
        UserDisplayName                 = Get-Value $d 'userDisplayName'
        OperatingSystem                 = Get-Value $d 'operatingSystem'
        OSVersion                       = Get-Value $d 'osVersion'
        ComplianceState                 = [string](Get-Value $d 'complianceState')
        ComplianceGracePeriodExpiration = ConvertTo-UtcDate (Get-Value $d 'complianceGracePeriodExpirationDateTime')
        LastSyncUtc                     = $sync
        DaysSinceSync                   = if ($sync) { [int][math]::Floor(($now - $sync).TotalDays) } else { $null }
        IsEncrypted                     = Get-Value $d 'isEncrypted'
        OwnerType                       = Get-Value $d 'managedDeviceOwnerType'
        ManagementAgent                 = Get-Value $d 'managementAgent'
        EnrollmentType                  = Get-Value $d 'deviceEnrollmentType'
        EnrolledUtc                     = ConvertTo-UtcDate (Get-Value $d 'enrolledDateTime')
        Manufacturer                    = Get-Value $d 'manufacturer'
        Model                           = Get-Value $d 'model'
        SerialNumber                    = Get-Value $d 'serialNumber'
        JailBroken                      = Get-Value $d 'jailBroken'
        AzureADDeviceId                 = Get-Value $d 'azureADDeviceId'
        IntuneDeviceId                  = Get-Value $d 'id'
    })
}

# ---- Filters --------------------------------------------------------------------------------------------
$out = @($rows)
if ($NonCompliantOnly) { $out = @($out | Where-Object { $_.ComplianceState -ne 'compliant' }) }
if ($ComplianceState) { $out = @($out | Where-Object { $ComplianceState -contains $_.ComplianceState }) }
if ($OperatingSystem) {
    $out = @($out | Where-Object { $os = $_.OperatingSystem; @($OperatingSystem | Where-Object { $os -like $_ }).Count -gt 0 })
}
if ($PSBoundParameters.ContainsKey('NotSyncedInDays')) {
    $out = @($out | Where-Object { $null -eq $_.DaysSinceSync -or $_.DaysSinceSync -ge $NotSyncedInDays })
}
if ($UserPrincipalName) {
    $u = if ($UserPrincipalName -match '[*?]') { $UserPrincipalName } else { "*$UserPrincipalName*" }
    $out = @($out | Where-Object { $_.UserPrincipalName -like $u })
}
$out = @($out | Sort-Object ComplianceState, OperatingSystem, DeviceName)

# ---- Output ---------------------------------------------------------------------------------------------
if ($CsvPath) {
    $out | Export-Csv -Path $CsvPath -NoTypeInformation -Encoding UTF8
    Write-Status ("CSV written: {0} ({1} rows)" -f $CsvPath, $out.Count)
}
if ($HtmlPath) {
    $css = '<style>body{font-family:Segoe UI,Arial,sans-serif;font-size:13px}table{border-collapse:collapse}th,td{border:1px solid #ccc;padding:4px 6px;text-align:left}th{background:#eee}</style>'
    $pre = "<h2>Intune device compliance</h2><p>Tenant $($ctx.TenantId). Generated $(Get-Date -Format 'yyyy-MM-dd HH:mm'). Devices: $($out.Count).</p>"
    $out | Select-Object DeviceName, UserPrincipalName, OperatingSystem, OSVersion, ComplianceState, LastSyncUtc, DaysSinceSync, IsEncrypted, OwnerType, Model, SerialNumber |
        ConvertTo-Html -Head $css -PreContent $pre | Out-File -FilePath $HtmlPath -Encoding UTF8
    Write-Status "HTML written: $HtmlPath"
}
$summary = $out | Group-Object ComplianceState | Sort-Object Count -Descending | ForEach-Object { "{0} {1}" -f $_.Count, $_.Name }
Write-Status ("Devices reported: {0}. By state: {1}. Not encrypted: {2}." -f $out.Count, $(if ($summary) { $summary -join ', ' } else { 'none' }), @($out | Where-Object { $_.IsEncrypted -eq $false }).Count)

if ($PassThru) { return $out }
if (-not $CsvPath -and -not $HtmlPath) {
    $out | Select-Object DeviceName, UserPrincipalName, OperatingSystem, ComplianceState, LastSyncUtc, IsEncrypted | Format-Table -AutoSize
}
Version 1.0.0 · SHA-256 3b4bc546d53366fccc07f0f8d01a052f719c372b95a6ed69fa46a249ba835592
Download and verify on Linux or macOS
curl -fsSL -o Get-IntuneComplianceReport.ps1 https://scr.srvscripts.com/intune-device-compliance-report/Get-IntuneComplianceReport.ps1 && curl -fsSL https://scr.srvscripts.com/intune-device-compliance-report/Get-IntuneComplianceReport.ps1.sha256 | sha256sum -c
Download and verify in Windows PowerShell
Invoke-WebRequest -Uri 'https://scr.srvscripts.com/intune-device-compliance-report/Get-IntuneComplianceReport.ps1' -OutFile 'Get-IntuneComplianceReport.ps1'; if ((Get-FileHash 'Get-IntuneComplianceReport.ps1' -Algorithm SHA256).Hash -eq '3B4BC546D53366FCCC07F0F8D01A052F719C372B95A6ED69FA46A249BA835592') { 'OK: the file is intact' } else { 'MISMATCH: do not run this file' }
Copy the whole line. In Windows PowerShell, curl and sha256sum are not the Linux tools, so use the PowerShell line there.
Also on GitHub: github.com/srvscripts/scripts

Frequently asked questions

How do I export Intune device compliance with PowerShell?

Read GET /deviceManagement/managedDevices from Microsoft Graph and export complianceState with the device fields. This script does it, with filters for state, OS and last sync.

Which permission does the Intune compliance report need?

DeviceManagementManagedDevices.Read.All, plus an Intune role such as Read Only Operator for the person running it interactively.

What compliance states can a device have?

unknown, compliant, noncompliant, conflict, error, inGracePeriod and configManager.

How do I find devices that stopped syncing with Intune?

Run the script with -NotSyncedInDays 30. It lists devices whose lastSyncDateTime is at least 30 days old.

Does the script retire or wipe devices?

No. It only reads. Retire or wipe stale devices in the Intune admin center after you have checked the list.

Changelog

  • 1.0.0 — First release.

Free website test

Is your website set up right?

Check SSL, security headers, redirects, robots.txt, sitemap, llms.txt and security.txt in one test. It takes about 30 seconds.