# Fix for mismatch between Intune and Azure device compliance status

It happened to us several times that **Azure showed company devices as non-compliant, but Intune showed them as compliant**. And this wasn’t fixed over time.

This is quite a problem in case, you require device compliance in your Azure Conditional policies a.k.a. users will be denied access to protected resources.

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">You can check if you are affected using using PowerShell code mentioned in the <strong>Tips</strong> section</div>
</div>

Internet says that device compliance status is synchronized to Azure only when a change occurs. So in case, something unexpected happens on the cloud side, it can happen that Azure doesn’t retrieve new compliance data.

In such case, you have two options:

* force another compliance status change on the device (by deploying a compliance policy that cannot be fulfilled and then removing such assignment to get a compliant device again)
    
* use API to set device compliance state as required
    

I will focus on the latter method because it doesn’t take hours to fix the issue, but seconds.

---

# Solution

<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">Before we start I want to say that compliance can be set via the <a target="_self" rel="noopener noreferrer nofollow" href="https://aadinternals.com/post/mdm/#marking-device-compliant-option-2-aad-graph-api" style="pointer-events: none">AADInternals module</a> too. But my solution supports FIDO auth and uses native commands so is more readable.</div>
</div>

You can fix compliance mismatch issues on-demand, but I prefer an automated solution to make sure any future problems will be fixed automatically.

Therefore my solution uses

* Azure Automation to run the PowerShell code
    
* Managed identity with appropriate Graph API permissions
    
    * `Device.ReadWrite.All`, `DeviceManagementManagedDevices.Read.All`
        

Below is the PowerShell code that will create & set Automation Account, but you can do all the stuff manually. It uses a combination of the official [Az.Automation](https://www.powershellgallery.com/packages/Az.Automation) and my custom [AzureResourceStuff](https://www.powershellgallery.com/packages/AzureResourceStuff) modules.

```powershell
Install-Module Az.Accounts, Az.Automation, AzureResourceStuff, AzureApplicationStuff

Connect-AzAccount

$automationAccountName = "DeviceComplianceMismatchFixer"
$azureLocation = "westeurope"
$automationResourceGroupName = "<chooseOne>"
$customRuntimeName = "PSH72"
$runbookName = "fixer"
$scheduleName = "every_hour"

# create Azure Automation Account
"Creating Automation Account"
$automationAccount = New-AzAutomationAccount -Name $automationAccountName -Location $azureLocation -ResourceGroupName $automationResourceGroupName -AssignSystemIdentity


# create Runtime Environment
$param = @{
    runtimeName           = $customRuntimeName
    runtimeLanguage       = "PowerShell"
    runtimeVersion        = "7.2"
    resourceGroupName     = $automationResourceGroupName
    automationAccountName = $automationAccountName
}

"Creating custom environment runtime"
$customRuntime = New-AzureAutomationRuntime @param


# add required modules to the runtime
'Microsoft.Graph.Authentication', 'Microsoft.Graph.Identity.DirectoryManagement', 'Microsoft.Graph.DeviceManagement' | % {   
    $param = @{
        resourceGroupName     = $automationResourceGroupName
        automationAccountName = $automationAccountName
        runtimeName           = $customRuntimeName
        moduleName            = $_
        dontWait              = $true
    }
    if ($moduleVersion) {
        $param.moduleVersion = $moduleVersion
    }

    "Adding PSGallery module '$_'"
    New-AzureAutomationRuntimeModule @param
}

# create runbook
$param = @{
    Name                  = $runbookName
    Type                  = "PowerShell"
    ResourceGroupName     = $automationResourceGroupName
    AutomationAccountName = $automationAccountName
}

"Creating runbook"
$runbook = New-AzAutomationRunbook @param


# set runbook runtime
"Setting runbook runtime to '$customRuntimeName'"
$null = Set-AzureAutomationRunbookRuntime -resourceGroupName $automationResourceGroupName -automationAccountName $automationAccountName -runtimeName $customRuntimeName -runbookName $runbookName


# set runbook code
"Setting runbook content"
$runbookContent = @'
# fix mismatch in compliance status between Intune and Azure (Intune wins btw)
# requires scope: Device.ReadWrite.All, DeviceManagementManagedDevices.Read.All

$WHATIF = $false # set to $true if you don;t want to do any changes to your environment!

Connect-MgGraph -Identity

$azureDeviceList = Get-MgDevice -All -Property DeviceId, Id, IsCompliant

foreach ($device in (Get-MgDeviceManagementManagedDevice -All -Property DeviceName, AzureAdDeviceId, ComplianceState -Filter "ManagedDeviceOwnerType eq 'company'" | sort DeviceName)) {
    $deviceName = $device.DeviceName
    $intuneCompliance = $device.ComplianceState
    $id = $device.AzureAdDeviceId

    "Processing $deviceName ($id)"

    if ($intuneCompliance -in "compliant", "inGracePeriod") {
        $intuneIsCompliant = $true
    } else {
        $intuneIsCompliant = $false
    }

    $aadDevice = $azureDeviceList | ? DeviceId -EQ $id

    if (!$aadDevice) {
        " - Corresponding Azure record is missing!"
        continue
    }

    $aadDeviceId = $aadDevice.Id
    $azureIsCompliant = $aadDevice.IsCompliant

    if ($intuneIsCompliant -ne $azureIsCompliant) {
        # convert boolean to match intune status message
        switch ($azureIsCompliant) {
            $true { $azureIsCompliant = "compliant" }
            $false { $azureIsCompliant = "noncompliant" }
        }

        " - $deviceName has mismatch in compliance status! Intune: $intuneCompliance, Azure: $azureIsCompliant. Fixing"

        if ($azureIsCompliant -eq $null) {
            " - Unable to fix. Azure object doesn't have any compliance value a.k.a. is invalid."
            continue
        }

        if (!$WHATIF) {
            $body = @{
                isCompliant = $intuneIsCompliant
            }
            $url = "https://graph.microsoft.com/v1.0/devices/$aadDeviceId"

            # set compliance status
            Invoke-MgGraphRequest -Method PATCH -Uri $url -Body ($body | ConvertTo-Json)

            # check compliance status
            $device = Invoke-MgGraphRequest -Method GET -Uri $url
            if ($device.isCompliant -ne $intuneIsCompliant) {
                throw "Compliance wasn't changed!"
            }
        }
    }
}
'@

$null = Set-AzureAutomationRunbookContent -runbookName $runbookName -resourceGroupName $automationResourceGroupName -automationAccountName $automationAccountName -content $runbookContent -publish


# set runbook schedule
$param = @{
    Name                  = $scheduleName
    StartTime             = (Get-Date).AddMinutes(10)
    ResourceGroupName     = $automationResourceGroupName
    AutomationAccountName = $automationAccountName
    HourInterval          = 1
}

"Creating schedule '$scheduleName'"
$schedule = New-AzAutomationSchedule @param
"Linking schedule '$scheduleName' to the runbook"
$scheduleLink = Register-AzAutomationScheduledRunbook -ScheduleName $scheduleName -RunbookName $runbookName -ResourceGroupName $automationResourceGroupName -AutomationAccountName $automationAccountName


# grant Automation Account System Managed Identity required permissions
Grant-AzureServicePrincipalPermission -servicePrincipalId '<idOfTheManagedIdentity>' -permissionType application -permissionList Device.ReadWrite.All, DeviceManagementManagedDevices.Read.All
```

And there you have it! Your automation is now set up to address any future compliance mismatches 😎

You can check the runbook job output in the Azure portal to see what issues were fixed etc.

---

# Tips

## How to find devices with compliance status mismatch

```powershell
# Find devices with compliance status mismatch

'Microsoft.Graph.Authentication', 'Microsoft.Graph.Identity.DirectoryManagement', 'Microsoft.Graph.DeviceManagement' | % {
    Import-Module $_
}

Connect-MgGraph

$azureDeviceList = Get-MgDevice -All -Property DeviceId, Id, IsCompliant

foreach ($device in (Get-MgDeviceManagementManagedDevice -All -Property DeviceName, AzureAdDeviceId, ComplianceState -Filter "ManagedDeviceOwnerType eq 'company'" | sort DeviceName)) {
    $deviceName = $device.DeviceName
    $intuneCompliance = $device.ComplianceState
    $id = $device.AzureAdDeviceId

    "Processing $deviceName ($id)"

    if ($intuneCompliance -in "compliant", "inGracePeriod") {
        $intuneIsCompliant = $true
    } else {
        $intuneIsCompliant = $false
    }

    $aadDevice = $azureDeviceList | ? DeviceId -EQ $id

    if (!$aadDevice) {
        " - Corresponding Azure record is missing!"
        continue
    }

    $aadDeviceId = $aadDevice.Id
    $azureIsCompliant = $aadDevice.IsCompliant

    if ($intuneIsCompliant -ne $azureIsCompliant) {
        # convert boolean to match intune status message
        switch ($azureIsCompliant) {
            $true { $azureIsCompliant = "compliant" }
            $false { $azureIsCompliant = "noncompliant" }
        }

        " - $deviceName has mismatch in compliance status! Intune: $intuneCompliance, Azure: $azureIsCompliant. Fixing"
    }
}
```

## How to set device compliance status

```powershell
# Set device compliance status in Azure

# objectId of the Azure device
$aadDeviceId = '<deviceObjectId>'
# what should be the compliance status
$isCompliant = $true # $false

Connect-MgGraph -Scope 'Device.ReadWrite.All', 'DeviceManagementManagedDevices.Read.All'

$body = @{
    isCompliant = $isCompliant
}
$url = "https://graph.microsoft.com/v1.0/devices/$aadDeviceId"

# set compliance status
Invoke-MgGraphRequest -Method PATCH -Uri $url -Body ($body | ConvertTo-Json)

# check compliance status
$device = Invoke-MgGraphRequest -Method GET -Uri $url
if ($device.isCompliant -ne $isCompliant) {
    throw "Compliance wasn't changed!"
}
```
