Skip to main content

Command Palette

Search for a command to run...

Get Intune Compliance data using PowerShell leveraging Graph API

With device details & error messages

Updated
7 min read
Get Intune Compliance data using PowerShell leveraging Graph API
O

I work as System Administrator for more than 15 years now and I love to make my life easier by automating work & personal stuff via PowerShell (even silly things like food recipes list generation).

Getting compliance data from Intune web interface can be a pain in the ass.

Post from Ola Strom about getting compliance data from Intune using Graph API inspired me to create PowerShell functions for such task.


TL;DR

We will create App registration in Azure, grant it correct permissions, and use it for authentication when calling PowerShell functions New-IntuneAuthHeader, Get-IntuneOverallComplianceStatus and Get-IntuneDeviceComplianceStatus.


Table of Contents


Prerequisite

To programmatically access Intune API (Graph API), you have to create App Registration with correct permissions in your Azure first. I've used this nice tutorial to learn how to do it.

In a nutshell, you have to:

  1. Create App Registration
    • Head to https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredApps > New registration > Choose App name and click Register
  2. Add permission to this App
    • Open your newly created App > API permissions > Add a permission > Add following Application permissions image.png
    • Don't forget to Grant admin consent
  3. Generate App Secret
    • Again in you App settings open Certificates & secrets > New client secret Choose validity period and some meaningful description. Don't forget to safely store generated password! We will need it later for requests authentication.

Function for creating authentication header

Function New-IntuneAuthHeader is prerequisite for any of my following functions. It is used to create an authentication header that is then used to authenticate every REST api request. It requires your tenant name (contoso.onmicrosoft.com), App ID and App Secret (password created earlier) to generate such header.

Application ID can be found in Overview section of our App image.png TenantDomainName can be found in Overview section of your Azure Active Directory as Primary domain.

Function definition

function New-IntuneAuthHeader {
    <#
    .SYNOPSIS
    Function for generating header that can be used for authentication of Graph API requests.

    .DESCRIPTION
    Function for generating header that can be used for authentication of Graph API requests.

    .PARAMETER credential
    Credentials for Graph API authentication (AppID + AppSecret).

    .PARAMETER TenantDomainName
    Name of your Azure tenant.

    <yourtenantdomain>.onmicrosoft.com

    .EXAMPLE
    $header = New-IntuneAuthHeader -credential $cred
    $URI = 'https://graph.microsoft.com/v1.0/deviceManagement/managedDevices/'
    $managedDevices = (Invoke-RestMethod -Headers $header -Uri $URI -Method Get).value

    .NOTES
    https://adamtheautomator.com/powershell-graph-api/#AppIdSecret
    https://thesleepyadmins.com/2020/10/24/connecting-to-microsoft-graphapi-using-powershell/
    https://github.com/microsoftgraph/powershell-intune-samples
    #>

    [CmdletBinding()]
    [Alias("Get-IntuneAuthHeader")]
    param (
        [System.Management.Automation.PSCredential] $credential = (Get-Credential -Message "Enter AppID as UserName and AppSecret as Password"),

        [ValidateNotNullOrEmpty()]
        $tenantDomainName
    )

    if (!$credential) { throw "Credential is missing" }

    if (!$tenantDomainName) { throw "TenantDomainName is missing" }

    Write-Verbose "Getting token"

    $body = @{
        Grant_Type    = "client_credentials"
        Scope         = "https://graph.microsoft.com/.default"
        Client_Id     = $credential.username
        Client_Secret = $credential.GetNetworkCredential().password
    }

    $connectGraph = Invoke-RestMethod -Uri "https://login.microsoftonline.com/$tenantDomainName/oauth2/v2.0/token" -Method POST -Body $body

    $token = $connectGraph.access_token

    if ($token) {
        return @{ Authorization = "Bearer $($token)" }
    } else {
        throw "Unable to obtain token"
    }
}

Function for getting overall compliance data

Function Get-IntuneOverallComplianceStatus can be used to get compliance data for your whole environment.

This means it will return you a combination of these three Intune pages as one object.

Overall Compliance status: image.png Specific Compliance policy setting: image.png Problematic device compliance: image.png

Function definition

function Get-IntuneOverallComplianceStatus {
    <#
    .SYNOPSIS
    Function for getting overall device compliance status from Intune.

    .DESCRIPTION
    Function for getting overall device compliance status from Intune.

    .PARAMETER header
    Authentication header.

    Can be created via New-IntuneAuthHeader.

    .PARAMETER justProblematic
    Switch for outputting only non-compliant items.

    .EXAMPLE
    $header = New-IntuneAuthHeader
    Get-IntuneOverallComplianceStatus -header $header

    Will return compliance information for all devices in your Intune.

    .EXAMPLE
    $header = New-IntuneAuthHeader
    Get-IntuneOverallComplianceStatus -header $header -justProblematic

    Will return just non-compliant information for devices in your Intune.
    #>

    [CmdletBinding()]
    param (
        [hashtable] $header
        ,
        [switch] $justProblematic
    )

    if (!$header) {
        # authenticate
        $header = New-IntuneAuthHeader -ErrorAction Stop
    }

    # helper hashtable for storing devices compliance data
    # just for performance optimization
    $deviceComplianceData = @{}

    # get overall compliance policies per-setting status
    $URI = 'https://graph.microsoft.com/v1.0/deviceManagement/deviceCompliancePolicySettingStateSummaries'
    $complianceSummary = (Invoke-RestMethod -Headers $header -Uri $URI -Method Get).value
    $complianceSummary = $complianceSummary | select @{n = 'Name'; e = { ($_.settingName -split "\.")[-1] } }, nonCompliantDeviceCount, errorDeviceCount, conflictDeviceCount, id

    if ($justProblematic) {
        # preserve just problematic ones
        $complianceSummary = $complianceSummary | ? { $_.nonCompliantDeviceCount -or $_.errorDeviceCount -or $_.conflictDeviceCount }
    }

    if ($complianceSummary) {
        $complianceSummary | % {
            $complianceSettingId = $_.id

            Write-Verbose $complianceSettingId
            Write-Warning "Processing $($_.name)"

            # add help text, to help understand, what this compliance setting validates
            switch ($_.name) {
                'RequireRemainContact' { Write-Warning "`t- devices that haven't contacted Intune for last 30 days" }
                'RequireDeviceCompliancePolicyAssigned' { Write-Warning "`t- devices without any compliance policy assigned" }
                'ConfigurationManagerComplianceRequired' { Write-Warning "`t- devices that are not compliant in SCCM" }
            }

            # get devices, where this particular compliance setting is not ok
            $URI = "https://graph.microsoft.com/v1.0/deviceManagement/deviceCompliancePolicySettingStateSummaries/$complianceSettingId/deviceComplianceSettingStates?`$filter=NOT(state eq 'compliant')"
            $complianceStatus = (Invoke-RestMethod -Headers $header -Uri $URI -Method Get).value

            if ($justProblematic) {
                # preserve just problematic ones
                $complianceStatus = $complianceStatus | ? { $_.state -ne "compliant" }
            }

            # loop through all devices that are not compliant (get details) and output the result
            $deviceDetails = $complianceStatus | % {
                $deviceId = $_.deviceId
                $deviceName = $_.deviceName
                $userPrincipalName = $_.userPrincipalName

                Write-Verbose "Processing $deviceName with id: $deviceId and UPN: $userPrincipalName"

                #region get error details (if exists) for this particular device and compliance setting
                if (!($deviceComplianceData.$deviceName)) {
                    Write-Verbose "Getting compliance data for $deviceName"
                    $deviceComplianceData.$deviceName = Get-IntuneDeviceComplianceStatus -deviceId $deviceId -justProblematic -header $header
                }

                if ($deviceComplianceData.$deviceName) {
                    # get error details for this particular compliance setting
                    $errorDescription = $deviceComplianceData.$deviceName | ? { $_.setting -eq $complianceSettingId -and $_.userPrincipalName -eq $userPrincipalName -and $_.errorDescription -ne "No error code" } | select -ExpandProperty errorDescription
                }
                #endregion get error details (if exists) for this particular device and compliance setting

                # output result
                $_ | select deviceName, userPrincipalName, state, @{n = 'errDetails'; e = { $errorDescription } } | sort state, deviceName
            }

            # output result for this compliance setting
            [PSCustomObject]@{
                Name                    = $_.name
                NonCompliantDeviceCount = $_.nonCompliantDeviceCount
                ErrorDeviceCount        = $_.errorDeviceCount
                ConflictDeviceCount     = $_.conflictDeviceCount
                DeviceDetails           = $deviceDetails
            }
        }
    }
}

Example output

By calling:

$header = New-IntuneAuthHeader
Get-IntuneOverallComplianceStatus -header $header -justProblematic

You will get: image.png Output is a classic object, so you can format or filter it as you wish...


Function for getting given device compliance data

Function Get-IntuneDeviceComplianceStatus can be used to get specific device(s) compliance data.

Function definition

function Get-IntuneDeviceComplianceStatus {
    <#
    .SYNOPSIS
    Function for getting device compliance status from Intune.

    .DESCRIPTION
    Function for getting device compliance status from Intune.
    Devices can be selected by name or id. If omitted, all devices will be processed.

    .PARAMETER deviceName
    Name of device(s).

    Can be combined with deviceId parameter.

    .PARAMETER deviceId
    Id(s) of device(s).

    Can be combined with deviceName parameter.

    .PARAMETER header
    Authentication header.

    Can be created via New-IntuneAuthHeader.

    .PARAMETER justProblematic
    Switch for outputting only non-compliant items.

    .EXAMPLE
    $header = New-IntuneAuthHeader
    Get-IntuneDeviceComplianceStatus -header $header

    Will return compliance information for all devices in your Intune.

    .EXAMPLE
    $header = New-IntuneAuthHeader
    Get-IntuneDeviceComplianceStatus -header $header -deviceName PC-1, PC-2

    Will return compliance information for PC-1, PC-2 from Intune.
    #>

    [CmdletBinding()]
    param (
        [string[]] $deviceName,

        [string[]] $deviceId,

        [hashtable] $header,

        [switch] $justProblematic
    )

    $ErrorActionPreference = "Stop"

    if (!$header) {
        # authenticate
        $header = New-IntuneAuthHeader -ErrorAction Stop
    }

    if (!$deviceName -and !$deviceId) {
        # all devices will be processed
        Write-Warning "You haven't specified device name or id, all devices will be processed"
        $deviceId = (Invoke-RestMethod -Headers $header -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$select=id" -Method Get).value | select -ExpandProperty Id
    } elseif ($deviceName) {
        $deviceName | % {
            #TODO limit returned properties using select filter
            $id = (Invoke-RestMethod -Headers $header -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices?`$filter=deviceName eq '$_'" -Method Get).value | select -ExpandProperty Id
            if ($id) {
                Write-Verbose "$_ was translated to $id"
                $deviceId += $id
            } else {
                Write-Warning "Device $_ wasn't found"
            }
        }
    }

    $deviceId = $deviceId | select -Unique

    foreach ($devId in $deviceId) {
        Write-Verbose "Processing device $devId"
        # get list of all compliance policies of this particular device
        $deviceCompliancePolicy = (Invoke-RestMethod -Headers $header -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$devId')/deviceCompliancePolicyStates" -Method Get).value

        if ($deviceCompliancePolicy) {
            # get detailed information for each compliance policy (mainly errorDescription)
            $deviceCompliancePolicy | % {
                $deviceComplianceId = $_.id
                $deviceComplianceStatus = (Invoke-RestMethod -Headers $header -Uri "https://graph.microsoft.com/beta/deviceManagement/managedDevices('$devId')/deviceCompliancePolicyStates('$deviceComplianceId')/settingStates" -Method Get).value

                if ($justProblematic) {
                    $deviceComplianceStatus = $deviceComplianceStatus | ? { $_.state -ne "compliant" }
                }

                $name = (Invoke-RestMethod -Headers $header -Uri "https://graph.microsoft.com/beta/deviceManagement/manageddevices('$devId')?`$select=deviceName" -Method Get).deviceName

                $deviceComplianceStatus | select @{n = 'deviceName'; e = { $name } }, state, errorDescription, userPrincipalName , setting, sources
            }
        } else {
            Write-Warning "There are no compliance policies for $devId device"
        }
    }
}

Example output

By calling:

$header = New-IntuneAuthHeader
Get-IntuneDeviceComplianceStatus -deviceName ni-20-ntb, ng-34-ntb -header $header -justProblematic

You will get: image.png Output is a classic object, so you can format or filter it as you wish...


Useful TIP

Don't forget that you can get Graph URI for your request just by opening Intune page in your browser and using Developer mode (F12)! image.png


Useful links that inspired me

  • https://adamtheautomator.com/powershell-graph-api/#AppIdSecret
  • https://thesleepyadmins.com/2020/10/24/connecting-to-microsoft-graphapi-using-powershell/
  • https://docs.microsoft.com/en-us/graph/query-parameters#filter-parameter
  • https://github.com/microsoftgraph/powershell-intune-samples
P

Hi Ondrej Sebela I was trying to do similar thing but for device configurations so I know what devices have which configurations set properly and which failed. I was able to list some statuses "an block" but so far I did not manage to query status of configuration for particular device - well at least not through PS (I did end up digging into Intune using developer tools and checking how it displays the device state via standard GET/POST). Have you ever tried to dig this information using MgGraph modules?

O
  1. I don't have such function, but to get all intune policies for a given account check https://doitpshway.com/get-all-intune-policies-assigned-to-the-specified-account-using-powershell (uses https://doitpshway.com/get-all-intune-policies-using-powershell-and-graph-api in the background)
  2. if you know the correct API URI, you can easily translate it back to the corresponding cmdlet by Find-MgGraphCommand -Uri "https://graph.microsoft.com/v1.0/deviceManagement" (just replace the URI part)
  3. if you want to check policy status directly on the client itself, check https://doitpshway.com/get-a-better-intune-policy-report-part-3-final
  4. this is how I am getting other types of reports, so it might help https://doitpshway.com/get-intune-reports-using-powershell-leveraging-graph-api
P

Thanks Ondrej Sebela will check that. Coming from the other MDMs I share your frustration of terrible reporting capabilities of Intune :)

S

Very good: clever innovative idea, well laid-out and associated code quality appears very good.

1
S

Hi Ondrej,

Thanks for you published work, I've found it very useful so far.

I've chosen to use MSAL.PS to acquire a header, alongside a self-signed certificate, rather than the hard-coded secret.

Regardless of that: many people have environments large enough that their token will expire. Can you suggest a simple method of handling token refresh when calling your functions to avoid such scenarios?

Many thanks,

Stevie

For those interested in using MSAL.PS and certs instead of New-IntuneAuthHeader:

  • generate a self-signed certificate key-pair on your computer

  • add its public key to the App Reg you created as documented by Ondrej

  • install the MSAL.PS module

  • use Get-MsalToken to acquire a token

# store your thumbprint
$thumbprint = <your certificate's thumbprint> # this is visible in your registered app's Credentials and Secrets section once it has been added

# store the path to your private key based on the thumbprint
$path = "Cert:\CurrentUser\My\" + $thumbprint

# set a hashtable for the connection parameters
$connectionDetails = @{
            'TenantId'          = <your tenant Id>
            'ClientId'          = <your app's client ID>
            'ClientCertificate' = Get-Item -Path $path
        }

$token = Get-MsalToken @connectionDetails

# create an auth header from that token
 $header = @{
            'Authorization' = $token.CreateAuthorizationHeader()
        }

you can now use the above $header when calling either of Ondrej's awesome functions.

1
O

Thank you 🙂

According the expired header. I saw solution on the internet where custom new attribute is put into $header variable with time of the token generation. Not sure what the token lifetime is but before you use it, you can then check whether it is valid and if not generate the new one...

More from this blog

D

Do it PowerShell way :)

78 posts

With over 15 years of experience as a system administrator, I have a passion for automating workflows using PowerShell. I believe in sharing my creations with the community. Why not, right? :)