Automate Valid OS Builds in a Compliance Policy
Compliance Settings
Some time ago, Microsoft added the "Valid operating system builds" option in the device properties section of compliance policies. This new option can be used to speficy the compliant builds for each version of Windows seperately (e.g. Windows 10 21H2, Windows 10 22H2, Windows 11 23H2):
This is a powerful method of monitoring devices that are not receiving Windows updates. In combination with conditional acess, this is also a great improvement for a company's security.
But there is one thing: The valid builds have to be kept up to date manually. So every month, someone has to search for the correct build number and update the compliance policy.
Can't this be automated?
Fortunately, Microsoft recently added support for managing windows updates to the Graph API. Right now it's still in preview in the Graph "beta" endpoint (Does that mean alpha? 😛 ). Your organization also needs to have specific licenses to be able to use Graph API for windows updates:
- Windows 10/11 Enterprise E3 or E5 (included in Microsoft 365 F3, E3, or E5)
- Windows 10/11 Education A3 or A5 (included in Microsoft 365 A3 or A5)
- Windows Virtual Desktop Access E3 or E5
- Microsoft 365 Business Premium
So the basic idea is to get the build numbers of a specific number of recent cumulative updates and to use it as minimum or maximum valid build number in the compliance policy.
Let's look at an example: As I'm writing this, the August 2024 Cumulative Updates are the latest releases. Let's assume we want to allow devices to be on one of the last 3 cumulative updates. This means the August, Juli and June cumulative updates are valid builds. The script will query Graph API and grabs the build numbers of these 3 cumulative updates for a set of allowed operating systems. The build number of the June update will be used as the "minimal Version" and the August update will be the "maximum version" in the compliance policy.
The output of the script looks like this:
Windows 10 - 21H2: 10.0.19044.4529 - 10.0.19044.4780
Windows 10 - 22H2: 10.0.19045.4529 - 10.0.19045.4780
Windows 11 - 22H2: 10.0.22621.3737 - 10.0.22621.4037
Windows 11 - 23H2: 10.0.22631.3737 - 10.0.22631.4037
The resulting compliance policy:
Real life example
There are two things we have to find out:
- When do we have to run script
- How many cumulative updates should be treated as compliant?
We need to check the update ring policies to determine when the script must be executed and how many cumulative updates should be treated as "valid / compliant".
The first ring (test / pilot) is configured like this:
The earliest time at which devices can receive the latest cumulative update is therefore the Microsoft Patch Tuesday +1 day. The script should therefore be executed in the night from Patch Tuesday to Wednesday.
The "final / broad" ring is configured like this:
Theoretically, all devices should receive the update after a maximum of 41 days. This means the devices can be on one of the last three cumulative updates. If the "Quality update deferral period" + "Deadline for quality updates" + "Grace period" is smaller than 30 days, devices are only compliant on one of the last two CUs.
The Script
<###
Synopsis
This PowerShell script automates the management of Windows update compliance and quality update policies within an organization's environment using the Microsoft Graph API. The script is designed to help administrators ensure that devices are compliant with specific update policies, particularly focusing on the most recent cumulative updates.
Key Functions:
Configuration Settings:
The script defines how many cumulative updates (e.g., security or non-security) should be considered compliant, which versions of Windows should be handled, and whether newer builds are allowed.
It also configures whether to expedite quality updates and sets the parameters for enforcing reboots after updates.
Connecting to Azure and Microsoft Graph:
The script uses a managed identity to connect to Azure and retrieve credentials from an Azure Key Vault.
It then connects to the Microsoft Graph API using these credentials.
Retrieving Update Information:
The script fetches information about the most recent cumulative updates from the Microsoft Graph API, filtered by the specified update classification (security or non-security).
It identifies the relevant Windows versions and their respective update details.
Building Compliance Policies:
The script creates a list of valid operating system build ranges based on the updates retrieved.
This list is used to update an existing Windows compliance policy, ensuring that devices running specified builds are treated as compliant.
Updating Quality Update Policies:
If the option to expedite updates is enabled, the script updates the quality update policy with the release date of the latest update and configures the enforced reboot timeline.
Error Handling:
The script includes error handling to manage exceptions during API calls, ensuring that issues are logged and the script exits gracefully if critical errors occur.
Intended Use:
This script is intended for IT administrators managing large Windows environments who need to enforce compliance with specific update policies and ensure that critical updates are installed in a timely manner.
Author: Max Weber - intune-blog.com
Date: 2024/08/06
###>
# Set Error Action Preference to Stop
$ErrorActionPreference = 'Stop'
# How many cumulative updates should be treated as compliant?
$numberOfUpdates = 3
Write-Verbose "Number of Updates treated as compliant: $numberOfUpdates"
# Allow all new builds by setting highestVersion to 10.0.*****.9999
$allowNewerBuilds = $true
Write-Verbose "Allow Newer Builds: $allowNewerBuilds"
# Set this to nonSecurity if you are deploying the monthly cumulative update preview updates [security / nonSecurity]
$updateClassification = 'security'
Write-Verbose "Update Classification: $updateClassification"
# Id of the compliance policy to update
$compliancePolicyId = 'bbd4352f-76cc-4417-b845-1da8bc8d68d1'
Write-Verbose "Compliance Policy Id: $compliancePolicyId"
# Define the Windows versions that should be handled
$validBuildNumbers = @('19044', '19045', '22621', '22631','26100')
Write-Verbose "Windows Version that will be handled: $validBuildNumbers"
# Enable update of Quality Update Policy to expedite update installation
$expediteQualityUpdate = $true
Write-Verbose "Expedite Quality Update Policy enabled: $expediteQualityUpdate"
# Id of the Quality Update policy
$qualityUpdatePolicyId = '97678ed0-c04d-4299-b861-170b1c63e3fc'
Write-Verbose "Quality Update Policy Id: $qualityUpdatePolicyId"
# Prefix for the display name of the quality update policy
$qualityUpdatePolicyDisplayNamePrefix = 'WIN - Updates - Expedite Windows Updates - D - '
Write-Verbose "Quality Update Policy Display Name prefix: $qualityUpdatePolicyDisplayNamePrefix"
# Days until reboot is enforced (0-2)
$qualityUpdatePolicyDaysUntilReboot = 1
Write-Verbose "Quality Update Policy days until reboot: $qualityUpdatePolicyDaysUntilReboot"
try {
Write-Verbose "Establishing Connection to Graph API"
# Connecto Managed Identity
Connect-AzAccount -Identity | Out-Null
Update-AzConfig -DisplaySecretsWarning $false | Out-Null
# Get Credentials from Key Vault
$VaultName = 'intune-blog-vault'
$TenantId = Get-AzKeyVaultSecret -VaultName $VaultName -Name 'tenantid' -AsPlainText
$ClientId = Get-AzKeyVaultSecret -VaultName $VaultName -Name 'clientid' -AsPlainText
$ClientSecretCredential = Get-AzKeyVaultSecret -VaultName $VaultName -Name 'clientsecret' -AsPlainText
$Creds = [System.Management.Automation.PSCredential]::new($ClientId, (ConvertTo-SecureString $ClientSecretCredential -AsPlainText -Force))
Connect-MgGraph -TenantId $TenantId -ClientSecretCredential $creds -NoWelcome
# For local testing
#Connect-MgGraph -Scopes WindowsUpdates.ReadWrite.All,DeviceManagementConfiguration.ReadWrite.All
Write-Verbose "Connection to Graph API established"
}
catch {
Write-Error "Failed to connect to MgGraph: $_"
Exit 1
}
####################################################################################################################
# Get Update Release Information
try {
#Create filter statement
Write-Verbose "Creating filter Statement for Windows Update query"
$filter = '$filter=microsoft.graph.windowsUpdates.qualityUpdateCatalogEntry/qualityUpdateClassification eq ''' + $updateClassification + '''&'
$uri = 'https://graph.microsoft.com/beta/admin/windows/updates/catalog/entries?$select=microsoft.graph.windowsUpdates.qualityUpdateCatalogEntry/productRevisions&$expand=microsoft.graph.windowsUpdates.qualityUpdateCatalogEntry/productRevisions&' + $filter + '$orderby=releaseDateTime%20desc&$top=' + $numberOfUpdates
Write-Verbose "Request URI: $uri"
$productRevisions = $(Invoke-MgGraphRequest -Method GET -Uri $uri).value.productRevisions
Write-Verbose "Successfully received update data"
}
catch {
Write-Error "Failed to get update data. Do you have the required license? Error: $_"
Exit 1
}
# Get required data for each Windows version
$validOperatingSystemBuildRanges = @()
$expediteQualityUpdateReleaseTime = ""
Write-Verbose "Constructing valid builds object for compliance policy"
$validBuildNumbers | ForEach-Object {
[array]$versions = $productRevisions | Where-Object id -match "10.0.$_" | Sort-Object -Property releaseDateTime
if($versions) {
$highestVersion = $versions[-1]
$lowestVersion = $versions[0]
$buildRangeObject = @{
"@odata.type" = "microsoft.graph.operatingSystemVersionRange";
"description" = "$($highestVersion.product) - $($highestVersion.version)";
"lowestVersion" = $lowestVersion.Id.toString();
"highestVersion" = $allowNewerBuilds ? "10.0.$_.9999" : $highestVersion.Id.toString();
}
$validOperatingSystemBuildRanges += $buildRangeObject
if(-not $expediteQualityUpdateReleaseTime) {
$expediteQualityUpdateReleaseTime = $lowestVersion.releaseDateTime
}
Write-Verbose "$($highestVersion.product) - $($highestVersion.version): $($buildRangeObject.lowestVersion) - $($buildRangeObject.highestVersion)"
Clear-Variable lowestVersion, highestVersion, buildRangeObject, versions
}
else {
Write-Error "No Updates found for $_"
}
}
if (-not $validOperatingSystemBuildRanges -or (-not $validOperatingSystemBuildRanges.Count -gt 0)) {
Write-Error "No valid updates found! Exiting script"
Exit 1
}
# Patch Compliance Policy
Write-Verbose "Updating Compliance Policy"
$compliancePolicyBody = @{
"@odata.type" = "#microsoft.graph.windows10CompliancePolicy";
"validOperatingSystemBuildRanges" = $validOperatingSystemBuildRanges
}
try {
Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/beta/deviceManagement/deviceCompliancePolicies/$compliancePolicyId" -Body $compliancePolicyBody
Write-Verbose "Successfully updated Compliance Policy"
}
catch {
Write-Error "Failed to update compliance policy: $_"
}
# Patch Quality Update Policy
if ($expediteQualityUpdate) {
Write-Verbose "Updating Expedite Quality Update Policy"
$expediteQualityUpdateBody = @{
"displayName" = $qualityUpdatePolicyDisplayNamePrefix + $(Get-Date $([DateTime]$expediteQualityUpdateReleaseTime) -Format "yyyy.MM")
"expeditedUpdateSettings" = @{
"daysUntilForcedReboot" = $qualityUpdatePolicyDaysUntilReboot;
"qualityUpdateRelease" = $expediteQualityUpdateReleaseTime;
}
}
Write-Verbose "Display Name: $($expediteQualityUpdateBody.displayName)"
Write-Verbose "Quality Update Release: $($expediteQualityUpdateBody.expeditedUpdateSettings.qualityUpdateRelease)"
try {
Invoke-MgGraphRequest -Method PATCH -Uri "https://graph.microsoft.com/beta/deviceManagement/windowsQualityUpdateProfiles/$qualityUpdatePolicyId" -Body $expediteQualityUpdateBody
Write-Verbose "Successfully updated Expedite Quality Update Policy"
}
catch {
Write-Error "Failed to update Expedite Quality Update policy: $_"
}
}
Write-Verbose "Script execution finished"
Customization
From line 37 to line 58, there are several variables that must be adapted to your environment.
Line 40 $numberOfUpdates
As explained earlier in the post, this value has to be adjusted accordingly to the update ring settings in your tenantLine 44 $allowNewerBuilds
Instead of using the build number of the latest CU as maximum valid version, "10.0.*****.9999 is always used. This way all newer builds are allowed. This can be used when you're deploying preview updates.Line 48 $updateClassification
If you are deploying preview updates in your environment, you can set this to "nonSecurity". If you're doing so, I recommend to have seperate scripts and compliance policies for devices that receive the security resp. non-security updates (security = regular cumulative updates / nonSecurity = preview of next months cumulative updates)Line 52 $compliancePolicyId
The Id of the compliance policy that should be changed by the automation. I recommend to use a seperate compliance policy only for the valid builds.Line 56 $validBuildNumbers
An array with all Windows versions that you want to allow and which should be checked by the scriptLine 60 $expediteQualityUpdate
To speed up the update deployment of those devices that are running a non compliant build, it is possible to have a "Expedite Quality Update" policy.Line 64 $qualityUpdatePolicyId
The id of the quality update policy to be altered (only required of $expediteQualityUpdate is $true).Line 68 $qualityUpdatePolicyDisplayNamePrefix
The prefix for the display name of the quality update policy. The minimum version will be added at the end of the name (only required of $expediteQualityUpdate is $true).Line 72 $qualityUpdatePolicyDaysUntilReboot
Days until a reboot is forced in the quality update policy (only required of $expediteQualityUpdate is $true).
Bonus: Automating a matching "Expedite Quality Updates" policy
Now that we have automated the compliance policy, it also makes sense to handle those devices that fall behind the minimum patch level. On line 52 of the script you can enable this functionality. By doing so, the specified quality update policy will always be synchronized with the minimal valid OS build.
Compliance Policy:
Quality Update Policy:
Scheduling the script
The script should be scheduled in an Azure automation account and run once a month on patch tuesday plus the number of days you are deffering updates for your first ring. I won't go into the details here on how to set up an automation account, since this has already been descriped in various blog posts. One very helpful article can be found here:
In your app registration you need to grant these permissions:
- WindowsUpdates.ReadWrite.All
- DeviceManagementConfiguration.ReadWrite.All
Add this module to the automation account:
- Microsoft.Graph.Authentication
Amazing Grace Period
Depending on the conditional access policies in your environment, it can have heavy user impact if a user's device becomes "non compliant" because of missing updates. That's why I recommend giving users enough time to update their devices and to send them several reminder mails.
Hand over responsibility
I like the idea of modern device management that holds users accountable for the (update) health of their devices. How many times have we had to beg users to reboot their computers just to get them “green” in our update compliance reports? With compliance policies, they are proactively notified and have to take action themselves!
If you have any questions or issues with the script, feel free to contact me at input(at)intune-blog.com!