Windows Autopatch for everyone

In the last post we’ve seen why patching is so important and how it is done in Microsoft Intune with Windows Update for Business. But how do we update all the other products like Microsoft 365, Microsoft Edge, and more? There are multiple ways to achieve that. With more expensive licenses like Microsoft E3 and E5 with Windows 10/11 Enterprise E3 and higher included, you could do that with a few clicks, give Microsoft some support contacts of your organization and Microsoft (Intune) will create the rest for you in the background. Let’s find out what feature I’m talking about and how to make it or parts of it available for everyone. Let’s dive in into my first “advanced” post after the basics, and explore my way of making Windows Autopatch available for everyone.

What’s Windows Autopatch?

How it started

A key feature in every Microsoft Intune project is Windows Update as explained in this post.

To create update-rings, driver update policies, and from time to time some feature update policiesis essential. Everything is automatic and everything works fine. Most of the time there are some groups, each of them for one (feature)update-ring and for every ring an exclusion group, in case you want to move a device or a device group to a different update-ring. Maybe you even have the same groups for your update policies for Microsoft 365 and Microsoft Edge. So, for bigger customers with Microsoft 365 E3 and E5 licenses it was an “easy game”, after approval of the customer, I was setting up Windows Autopatch, followed by some adjustments in settings and fully outsourced, fully automatic and intelligent update platform was running. But what about smaller companies without “expensive” licenses, you may ask…? Recently I asked myself the same question. Is there a creative way to build something similar to Windows Autopatch, but for everyone? You want a spoiler? Yes it is, partially.

I began with a brainstorming session and made a simple mindmap with the (essential) features of my adoption of Windows Autopatch. (Sorry for my bad handwriting)

Windows Autopatch Brainstorming

I decided to sort all the features in the following categories: Must-have features and Extra features. More on that in the next chapter.

Features of Autopatch

Must-have Features

These features have to be included.

Dynamic Onboarding

Similar to Windows Autopatch my version should support an easy to use and dynamic onboarding process based on PowerShell. It has to be possible without any remediation scripts or 3rd party tools.

Dynamic and adjustable Group Assignment

After onboarding a device it should already be dynamically assigned to an update-ring and policy but it should also be possible to change that without any “Exclusion-Groups”. The effort of doing that has to be minimized.

Same or better Update Capability as Windows Autopatch

It should be possible to deploy:

  • All kind of Windows-Updates (including explicit feature update policies)
  • Driver Updates (but sadly not for everyone because of licensing)
  • Updates for Microsoft 365 with different update settings
  • Updates for Microsoft Edge with different update settings

Additionally other products should be included as well, such as:

  • Google Chrome
  • Mozilla Firefox
  • WinGet Apps

It has to be easy to integrate more apps with ADMX-templates, build in setting-catalog or custom policies.

Reporting

With the build-in Windows Update report in Microsoft Intune it’s already possible to see the update-progress for: feature updates, quality updates and driver updates. Additionally we’ll use the in Intune 2404 release announced Windows update distribution report. Using this we have a powerful report functionality built-in!

Naming

There should be a naming convention for groups, policies and everything else. But it should be easy to read and remember.

Extra Features

These feature(s) would be nice-to-have, but aren’t essential.

PowerShell Console for Management

Windows Autopatch is fully integrated in Microsoft Intune Admin Center. It’s not possible to do the same for my solution, but a management console in PowerShell would be nice.

My Idea and Solution

Idea 1: The Concept with Grouptags

RTC Patch Concept

The idea is to define the whole patch-management with one single and easy group-tag assignment within Windows Autopilot. Like this you don’t need any exclusion-groups. After assigning the tag every other assignment works automatically and your devices are getting updated!

Idea 2: The Concept with Entra ID Device Extension Attributes

RTC Patch Concept Attributes

After posting this blogpost in the amazing Modern Endpoint Management LinkedIn-Community, I got feedback from John. He mentioned the limitations of my approach based on grouptags and suggested Entra ID Device Extension Attributes instead and I fully agree.

Sometimes, especially when you’re designing your own solution, you’re sooo focused on your own approach that you forget about other possible solutions, and that’s where the power of our community comes in. Based on feedback, we make this project even better together <3

So I decided to update my blog with the approach of creating a patch-management with one single and easy Entra ID Device Extension value. Using this approach, you also don’t need any exclusion-groups. After assigning the Entra ID Extension Attribute value, every other assignment works automatically and your devices are getting updated like in the first approach! I decided to use Entra ID Device Extension Attribute 5. I assume that the first 4 extension attributes are already in use in your environment.

Prerequisites

Licensing

You basically need a license with Microsoft Intune included and at least Entra ID P1 for Dynamic Groups and automatic Intune Device enrollment.

Intune

First Part – The Name(s)

I’m not a fan of fancy names . I call my solution “RTC-Patch”. Simple, right?

Naming convention

The idea is that every group, every policy and every script starts with “RTC-Patch” followed by type and details.

Example

Patch Ring 1 will be called:
RTC-Patch-Ring-1

Patch Policy for Test-Ring will be called:
RTC-Patch-Policy-Test

Second Part – Groups and Policies

The Groups

Dynamic and Static Entra ID Groups for Idea 1 (Grouptags)

To create dynamic groups we need Microsoft Graph API and the following module: Microsoft.Graph.Beta.Groups
Of course it would be possible to create the groups by hand. But let’s be efficient.

To make onboarding efficient we need to create the following dynamic Entra ID groups. Every group has a dynamic membership rule based on grouptags / physical ID’s:

Group NameGrouptagDynamic membership rule
RTC-Patch-Ring-TestPatch-Test(device.devicePhysicalIds -any (_ -eq “[OrderID]:Patch-Test”))
RTC-Patch-Ring-1Patch-1(device.devicePhysicalIds -any (_ -eq “[OrderID]:Patch-1”))
RTC-Patch-Ring-2Patch-2(device.devicePhysicalIds -any (_ -eq “[OrderID]:Patch-2”))
RTC-Patch-Ring-3Patch-3(device.devicePhysicalIds -any (_ -eq “[OrderID]:Patch-3”))
RTC-Patch-Ring-LastPatch-Last(device.devicePhysicalIds -any (_ -eq “[OrderID]:Patch-Last”))

To assign policies we create even more groups. Every previously created dynamic Entra ID group will be a member of one of the following static policy-groups.

Group NameDescriptionMembergroup
RTC-Patch-Policy-TestThis Group will receive beta updates to test.RTC-Patch-Ring-Test
RTC-Patch-Policy -FastThis group will receive regular production updates.RTC-Patch-Ring-1, RTC-Patch-Ring-2
RTC-Patch-Policy -BroadThis group will receive Long-Term Service Releases for the best stability.RTC-Patch-Ring-3, RTC-Patch-Ring-Last

To make it easy, I created a PowerShell script, which will create the groups for you, including the assignment.

function Install-ModuleIfNotPresent {
    param (
        [string]$ModuleName
    )

    $module = Get-Module -ListAvailable -Name $ModuleName

    if ($null -eq $module) {
        try {
            Install-Module -Name $ModuleName -AllowClobber -Scope CurrentUser -Force -Confirm:$false
            Write-Output "$ModuleName module installed successfully."
        }
        catch {
            Write-Error "Failed to install $ModuleName module. Please run PowerShell as Administrator."
            exit
        }
    }
    else {
        Write-Output "$ModuleName module is already installed."
    }
}

function Create-Group {
    param (
        [hashtable]$Params
    )

    New-MgBetaGroup -BodyParameter $Params
    Write-Output "Group $($Params.displayName) successfully created!"
}

function Add-GroupToGroup {
    param (
        [string]$dynamicGroupName,
        [string]$staticGroupName
    )

    # Get Object IDs for the Dynamic Group and Static Group
    $dynamicGroupId = (Get-MgBetaGroup -Filter "displayName eq '$dynamicGroupName'").Id
    $staticGroupId = (Get-MgBetaGroup -Filter "displayName eq '$staticGroupName'").Id

    # Prepare the member body
    $memberBody = @{
        "@odata.id" = "https://graph.microsoft.com/beta/directoryObjects/$dynamicGroupId"
    }

    # Add the Dynamic Group to the Static Group
    New-MgBetaGroupMemberByRef -GroupId $staticGroupId -BodyParameter $memberBody
    Write-Output "Added $dynamicGroupName to $staticGroupName"
}

# Main script
$moduleName = "Microsoft.Graph.Beta.Groups"
Install-ModuleIfNotPresent -ModuleName $moduleName

Import-Module $moduleName
Disconnect-MgGraph
Connect-MgGraph -Scopes "Group.ReadWrite.All"

# list of groups to create
$groupsToCreate = @(
    @{
        description                   = "Dynamic RTC-Patch Group with all Windows Autopilot Devices"
        displayName                   = "RTC-Patch-AllAutopilotDevices"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-AllAutopilotDevices"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIDs -any (_ -startsWith "[ZTDid]"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring Test"
        displayName                   = "RTC-Patch-Ring-Test"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-Test"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIds -any (_ -eq "[OrderID]:Patch-Test"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring 1"
        displayName                   = "RTC-Patch-Ring-1"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-1"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIds -any (_ -eq "[OrderID]:Patch-1"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring 2"
        displayName                   = "RTC-Patch-Ring-2"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-2"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIds -any (_ -eq "[OrderID]:Patch-2"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring 3"
        displayName                   = "RTC-Patch-Ring-3"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-3"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIds -any (_ -eq "[OrderID]:Patch-3"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring Last"
        displayName                   = "RTC-Patch-Ring-Last"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-Last"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIds -any (_ -eq "[OrderID]:Patch-Last"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description     = "Static Group for Edge and Microsoft365 Updates"
        displayName     = "RTC-Patch-Policy-Test"
        groupTypes      = @()
        mailEnabled     = $false
        mailNickname    = "RTC-PatchPolicy-Test"
        securityEnabled = $true
    },
    @{
        description     = "Static Group for Edge and Microsoft365 Updates"
        displayName     = "RTC-Patch-Policy-Fast"
        groupTypes      = @()
        mailEnabled     = $false
        mailNickname    = "RTC-PatchPolicy-Fast"
        securityEnabled = $true
    },
    @{
        description     = "Static Group for Edge and Microsoft365 Updates"
        displayName     = "RTC-Patch-Policy-Broad"
        groupTypes      = @()
        mailEnabled     = $false
        mailNickname    = "RTC-PatchPolicy-Broad"
        securityEnabled = $true
    }
)

foreach ($groupParams in $groupsToCreate) {
    Create-Group -Params $groupParams
}


#Group assignment dynamic into static-group

# Group RTC-Patch-Ring-Test into Group RTC-PatchPolicy-Test
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-Test" -staticGroupName "RTC-Patch-Policy-Test"

# Group RTC-Patch-Ring-1 into Group RTC-PatchPolicy-Fast
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-1" -staticGroupName "RTC-Patch-Policy-Fast"

# Group RTC-Patch-Ring-2 into Group RTC-PatchPolicy-Fast
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-2" -staticGroupName "RTC-Patch-Policy-Fast"

# Group RTC-Patch-Ring-3 into Group RTC-PatchPolicy-Broad
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-3" -staticGroupName "RTC-Patch-Policy-Broad"

# Group RTC-Patch-Ring-Last into Group RTC-PatchPolicy-Broad
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-Last" -staticGroupName "RTC-Patch-Policy-Broad"

Information

As always, if you see something that could be done more efficiently or better, please feel free to comment or send me a message on LinkedIn / X.

Dynamic and Static Entra ID Groups for Idea 2 (Entra ID Device Extension Attributes)

To create dynamic groups we need Microsoft Graph API and the following module: Microsoft.Graph.Beta.Groups
Of course it would be possible to create the groups by hand. But let’s be efficient.

To make onboarding efficient we need to create the following dynamic Entra ID groups. Every group has a dynamic membership rule based on device extensionAttribute:

Group NameGrouptagDynamic membership rule
RTC-Patch-Ring-TestPatch-Test(device.extensionAttribute5 -eq “Patch-Test”)
RTC-Patch-Ring-1Patch-1(device.extensionAttribute5 -eq “Patch-1”)
RTC-Patch-Ring-2Patch-2(device.extensionAttribute5 -eq “Patch-2”)
RTC-Patch-Ring-3Patch-3(device.extensionAttribute5 -eq “Patch-3”)
RTC-Patch-Ring-LastPatch-Last(device.extensionAttribute5 -eq “Patch-Last”)

To assign policies we create even more groups. Every previously created dynamic Entra ID group will be a member of one of the following static policy-groups.

Group NameDescriptionMembergroup
RTC-Patch-Policy-TestThis Group will receive beta updates to test.RTC-Patch-Ring-Test
RTC-Patch-Policy -FastThis group will receive regular production updates.RTC-Patch-Ring-1, RTC-Patch-Ring-2
RTC-Patch-Policy -BroadThis group will receive Long-Term Service Releases for the best stability.RTC-Patch-Ring-3, RTC-Patch-Ring-Last

To make it easy, I created a PowerShell script, which will create the groups for you, including the assignment.

function Install-ModuleIfNotPresent {
    param (
        [string]$ModuleName
    )

    $module = Get-Module -ListAvailable -Name $ModuleName

    if ($null -eq $module) {
        try {
            Install-Module -Name $ModuleName -AllowClobber -Scope CurrentUser -Force -Confirm:$false
            Write-Output "$ModuleName module installed successfully."
        }
        catch {
            Write-Error "Failed to install $ModuleName module. Please run PowerShell as Administrator."
            exit
        }
    }
    else {
        Write-Output "$ModuleName module is already installed."
    }
}

function Create-Group {
    param (
        [hashtable]$Params
    )

    New-MgBetaGroup -BodyParameter $Params
    Write-Output "Group $($Params.displayName) successfully created!"
}

function Add-GroupToGroup {
    param (
        [string]$dynamicGroupName,
        [string]$staticGroupName
    )

    # Get Object IDs for the Dynamic Group and Static Group
    $dynamicGroupId = (Get-MgBetaGroup -Filter "displayName eq '$dynamicGroupName'").Id
    $staticGroupId = (Get-MgBetaGroup -Filter "displayName eq '$staticGroupName'").Id

    # Prepare the member body
    $memberBody = @{
        "@odata.id" = "https://graph.microsoft.com/beta/directoryObjects/$dynamicGroupId"
    }

    # Add the Dynamic Group to the Static Group
    New-MgBetaGroupMemberByRef -GroupId $staticGroupId -BodyParameter $memberBody
    Write-Output "Added $dynamicGroupName to $staticGroupName"
}

# Main script
$moduleName = "Microsoft.Graph.Beta.Groups"
Install-ModuleIfNotPresent -ModuleName $moduleName

Import-Module $moduleName
Disconnect-MgGraph
Connect-MgGraph -Scopes "Group.ReadWrite.All"

# list of groups to create
$groupsToCreate = @(
    @{
        description                   = "Dynamic RTC-Patch Group with all Windows Autopilot Devices"
        displayName                   = "RTC-Patch-AllAutopilotDevices"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-AllAutopilotDevices"
        securityEnabled               = $true
        membershipRule                = '(device.devicePhysicalIDs -any (_ -startsWith "[ZTDid]"))'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring Test"
        displayName                   = "RTC-Patch-Ring-Test"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-Test"
        securityEnabled               = $true
        membershipRule                = '(device.extensionAttribute5 -eq "Patch-Test")'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring 1"
        displayName                   = "RTC-Patch-Ring-1"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-1"
        securityEnabled               = $true
        membershipRule                = '(device.extensionAttribute5 -eq "Patch-1")'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring 2"
        displayName                   = "RTC-Patch-Ring-2"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-2"
        securityEnabled               = $true
        membershipRule                = '(device.extensionAttribute5 -eq "Patch-2")'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring 3"
        displayName                   = "RTC-Patch-Ring-3"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-3"
        securityEnabled               = $true
        membershipRule                = '(device.extensionAttribute5 -eq "Patch-3")'
        membershipRuleProcessingState = "on"
    },
    @{
        description                   = "Dynamic RTC-Patch Group with devices assigned to Ring Last"
        displayName                   = "RTC-Patch-Ring-Last"
        groupTypes                    = @("DynamicMembership")
        mailEnabled                   = $false
        mailNickname                  = "RTC-Patch-Ring-Last"
        securityEnabled               = $true
        membershipRule                = '(device.extensionAttribute5 -eq "Patch-Last")'
        membershipRuleProcessingState = "on"
    },
    @{
        description     = "Static Group for Edge and Microsoft365 Updates"
        displayName     = "RTC-Patch-Policy-Test"
        groupTypes      = @()
        mailEnabled     = $false
        mailNickname    = "RTC-PatchPolicy-Test"
        securityEnabled = $true
    },
    @{
        description     = "Static Group for Edge and Microsoft365 Updates"
        displayName     = "RTC-Patch-Policy-Fast"
        groupTypes      = @()
        mailEnabled     = $false
        mailNickname    = "RTC-PatchPolicy-Fast"
        securityEnabled = $true
    },
    @{
        description     = "Static Group for Edge and Microsoft365 Updates"
        displayName     = "RTC-Patch-Policy-Broad"
        groupTypes      = @()
        mailEnabled     = $false
        mailNickname    = "RTC-PatchPolicy-Broad"
        securityEnabled = $true
    }
)

foreach ($groupParams in $groupsToCreate) {
    Create-Group -Params $groupParams
}


#Group assignment dynamic into static-group

# Group RTC-Patch-Ring-Test into Group RTC-PatchPolicy-Test
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-Test" -staticGroupName "RTC-Patch-Policy-Test"

# Group RTC-Patch-Ring-1 into Group RTC-PatchPolicy-Fast
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-1" -staticGroupName "RTC-Patch-Policy-Fast"

# Group RTC-Patch-Ring-2 into Group RTC-PatchPolicy-Fast
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-2" -staticGroupName "RTC-Patch-Policy-Fast"

# Group RTC-Patch-Ring-3 into Group RTC-PatchPolicy-Broad
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-3" -staticGroupName "RTC-Patch-Policy-Broad"

# Group RTC-Patch-Ring-Last into Group RTC-PatchPolicy-Broad
Add-GroupToGroup -dynamicGroupName "RTC-Patch-Ring-Last" -staticGroupName "RTC-Patch-Policy-Broad"

Policies

Update-Rings

I decided to stay as close as possible to the default Update-Ring Configuration of Windows Autopatch. So we create the following 5 Update-Rings:

RTC Patch Ring Test Microsoft Intune admin center

Group Assignment: RTC-Patch-Ring-Test

RTC Patch Ring 1 Microsoft Intune admin center

Group Assignment: RTC-Patch-Ring-1

RTC Patch Ring 2 Microsoft Intune admin center

Group Assignment: RTC-Patch-Ring-2

RTC Patch Ring 3 Microsoft Intune admin center

Group Assignment: RTC-Patch-Ring-3

RTC Patch Ring Last Microsoft Intune admin center

Group Assignment: RTC-Patch-Ring-4

Information

Please adjust the settings and parameters according to your company’s needs.

Download

You can download all Update-Rings as JSON files from my GitHub and import them for example with Intune Manager from Micke-K and cstaubli.

Driver-Update-Rings

I created Driver-Update-Rings according to Windows-Update-Rings. They are set to automatic update and have the same names, the same deferral periods and the same RTC-Policy group assigned to them.

Important Information

Not every license includes driver update policies / management.
One of the following licenses is necessary :

  • Windows 10/11 Enterprise E3 or E5
  • Windows 10/11 Education A3 or A5
  • Windows Virtual Desktop Access E3 or E5
  • Microsoft 365 Business Premium

Of course also every other license (bundle) that includes one of the mentioned licenses.

Download

You can download all Driver-Update-Rings as JSON files from my GitHub and import them for example with Intune Manager from Micke-K and cstaubli.

Web browsers

As described earlier, I want to include update policies for the following browsers:

  • Microsoft Edge
  • Google Chrome
  • Mozilla Firefox

To do that we first need to download and upload the required ADMX-Templates to Microsoft Intune. In this post you see how:

Here you’ll find the ADMX-Templates needed. Microsoft Edge is already built-in.

The goal is to include something like that for every web browser:

GroupUpdate-Channel
RTC-Patch-Policy-TestBeta / Preview
RTC-Patch-Policy-FastStable
RTC-Patch-Policy-BroadExtended Stable

Configurations

Beta / Preview

RTC Patch Edge Test Microsoft Intune admin center

Stable

RTC Patch Edge Fast Microsoft Intune admin center

Extended Stable

RTC Patch Edge Broad Microsoft Intune admin center

Beta / Preview

SettingValue
Allow installationForce Installs (Maschine-Wide)
Target Channel overridebeta
Update policy overrideAllways allow updates

Stable

SettingValue
Allow installationForce Installs (Maschine-Wide)
Target Channel overridestable
Update policy overrideAllways allow updates

Extended Stable

SettingValue
Allow installationForce Installs (Maschine-Wide)
Target Channel overridestable
Update policy overrideAllways allow updates

Information

Unfortunately there is no LTS Channel setting for Google Chrome in ADMX-Template.

Autoupdate

Unfortunately it’s not possible to set update-channels for normal Mozilla Firefox with ADMX-Templates. It’s only possible to enable autoupdate.

SettingValue
Application AutoupdateEnabled

You could make a package out of Firefox ESR and assign it to RTC-Patch-Policy-Broad. Please have a look at my blogpost about packaging.

Download

You can download all web browser update policies as JSON files from my GitHub and import them for example with Intune Manager from Micke-K and cstaubli or directly with Microsoft Intune.

Microsoft 365

Also included are Microsoft 365 update Policies.

GroupUpdate-Channel
RTC-Patch-Policy-TestCurrent Channel (Preview)
RTC-Patch-Policy-FastCurrent Channel
RTC-Patch-Policy-BroadMonthly Enterprise Channel
RTC Patch Microsoft365 Test Microsoft Intune admin center
RTC Patch Microsoft365 Fast Microsoft Intune admin center
RTC Patch Microsoft365 Broad Microsoft Intune admin center

Download

You can download all Microsoft 365 update policies as JSON files from my GitHub and import them for example with Intune Manager from Micke-K and cstaubli or directly with Microsoft Intune.

Winget Apps

There are multiple options to update Winget Apps. I decided to include a third party Microsoft Store App together with ADMX-Template configuration for that.

First you need to import the ADMX template, which you can get here.
Then we add the app “Winget-AutoUpdate-aaS” as a “Microsoft Store app (new)”
If you don’t know how to do that, please have a look at this post:

We assign the app to every Update-Ring Group:

Winget Select groups Microsoft Intune admin center

Now we create the policy “RTC-Patch-WinGet” and configure the following parameters:

SettingValue
Updates at logonEnabled
Notification levelNone
Startmenu ShortcutDisabled
Desktop ShortcutDisabled

Information

By default “Winget-AutoUpdate-aaS” will update every Winget application on the system.

There is also a blacklist of apps that you don’t want to update:

SettingValue
Application ListWinGet IDs

The policy will be assigned to every RTC-Patch-Policy Group. If you want to use the blacklist, feel free to create multiple policies and assign the to the necessary RTC-Patch-Policy Groups.

Third Part – Logic

Onboarding and Changes

Manually (with Grouptags)

If you really want to, it’s possible to manually assign the right group tag to your Windows Autopilot Device. You can assign these group-tags:

  • Patch-Test
  • Patch-1
  • Patch-2
  • Patch-3
  • Patch-Last

The needed group assignments in the background happen automatically.

Manually (with Entra ID Device Extension Attributes)

If you really want to, it’s possible to manually insert the right value into Entra ID Devices Extension attribute 5. You can set these values:

  • Patch-Test
  • Patch-1
  • Patch-2
  • Patch-3
  • Patch-Last

The needed group assignments in the background happen automatically.

Automated (with Grouptags)

To onboard devices automatically you can execute the following script. It will show you the available group tags to assign. After choosing the group tag you can select the target device(s) and then the previously chosen group tag will be assigned to these devices.

The following permissions are needed to make these changes:

  • DeviceManagementServiceConfig.ReadWrite.All
function Install-ModuleIfNotPresent {
    param (
        [string]$ModuleName
    )

    $module = Get-Module -ListAvailable -Name $ModuleName

    if ($null -eq $module) {
        try {
            Install-Module -Name $ModuleName -AllowClobber -Scope CurrentUser -Force -Confirm:$false
            Write-Output "$ModuleName module installed successfully."
        }
        catch {
            Write-Error "Failed to install $ModuleName module. Please run PowerShell as Administrator."
            exit
        }
    }
    else {
        Write-Output "$ModuleName module is already installed."
    }
}

function RTC-Patch-ChangeAssignment {
    # Get all autopilot devices (even if more than 1000)
    $autopilotDevices = Invoke-MSGraphRequest -HttpMethod GET -Url "deviceManagement/windowsAutopilotDeviceIdentities" | Get-MSGraphAllPages

    # Ask user to select a group tag to assign
    $groupTags = @("Patch-Test", "Patch-1", "Patch-2", "Patch-3", "Patch-Last")
    $selectedGroupTag = $groupTags | Out-GridView -OutputMode Single -Title "Select the group tag you want to assign"

    # Display gridview to show devices
    $selectedAutopilotDevices = $autopilotDevices | Out-GridView -OutputMode Multiple -Title "Select Windows Autopilot entities to update"

    $selectedAutopilotDevices | ForEach-Object {

        $autopilotDevice = $PSItem


        $autopilotDevice.groupTag = $selectedGroupTag


        $requestBody =
        @"
    {
        groupTag: `"$($autopilotDevice.groupTag)`",
    }
"@
        Write-Output "Updating entity: $($autopilotDevice.id) | groupTag: $($autopilotDevice.groupTag) | orderIdentifier: $($autopilotDevice.orderIdentifier)"
        Invoke-MSGraphRequest -HttpMethod POST -Content $requestBody -Url "deviceManagement/windowsAutopilotDeviceIdentities/$($autopilotDevice.id)/UpdateDeviceProperties" 
    }

    # Invoke an autopilot service sync
    Invoke-MSGraphRequest -HttpMethod POST -Url "deviceManagement/windowsAutopilotSettings/sync"
    
}

#Main Script
$moduleName = "Microsoft.Graph"
Install-ModuleIfNotPresent -ModuleName $moduleName

Disconnect-MgGraph
Connect-MSGraph
Update-MSGraphEnvironment -SchemaVersion "Beta" -Quiet
Connect-MSGraph -Quiet

RTC-Patch-ChangeAssignment

This is a modified version of a PowerShell script made by Nicola Suter. Thank you so much for the permission to use it in my post 🙂

Automated (with Entra ID Device Extension Attributes)

To onboard devices automatically you can execute the following script. It will show you the available values to assign to Entra ID device extension attribute 5. After choosing the value you can select the target device(s) and then the previously chosen value will be assigned to these device(s).

The following permissions are needed to make these changes:

  • Device.ReadWrite.All
function Install-ModuleIfNotPresent {
    param (
        [string]$ModuleName
    )

    $module = Get-Module -ListAvailable -Name $ModuleName

    if ($null -eq $module) {
        try {
            Install-Module -Name $ModuleName -AllowClobber -Scope CurrentUser -Force -Confirm:$false
            Write-Output "$ModuleName module installed successfully."
        }
        catch {
            Write-Error "Failed to install $ModuleName module. Please run PowerShell as Administrator."
            exit
        }
    }
    else {
        Write-Output "$ModuleName module is already installed."
    }
}

function RTC-Patch-ChangeAssignment {
    Connect-MgGraph -Scopes "Device.ReadWrite.All"

    # Retrieve all devices
    [array]$Devices = Get-MgDevice -All

    # Define group tags for selection
    $EAvalues = @("Patch-Test", "Patch-1", "Patch-2", "Patch-3", "Patch-Last")

    # Ask user to select a group tag to assign
    $selectedEAvalue = $EAvalues | Out-GridView -OutputMode Single -Title "Select the value you want to assign to Entra ID extension attribute"

    # Allow user to select one or more devices from a list to update
    $selectedDevices = $Devices | Out-GridView -OutputMode Multiple -Title "Select devices to update Entra ID extension attribute"

    foreach ($Device in $selectedDevices) {
        # Prepare the JSON payload for updating the device
        $Attributes = @{
            "extensionAttributes" = @{
                "extensionAttribute5" = $selectedEAvalue
            }
        } | ConvertTo-Json

        # Update the device with the new group tag
        Update-MgDevice -DeviceId $Device.Id -BodyParameter $Attributes

        Write-Host ("Device {0} has been updated with Entra ID extension attribute {1}" -f $Device.DisplayName, $selectedEAvalue)
    }
}

# Main Script
$moduleName = "Microsoft.Graph"
Install-ModuleIfNotPresent -ModuleName $moduleName
Disconnect-MgGraph
RTC-Patch-ChangeAssignment

Fourth Part – Extra

Management Console for Idea 1 (Grouptags)

It’s essential to have some kind of a management tool. In my case it’s a PowerShell script with some pre-built functions.

In detail the following functions are included:

  • Get every RTC-Patch device
  • Get current Ring assignment
  • Change Assignments of RTC-Patch devices
function Install-ModuleIfNotPresent {
    param (
        [string]$ModuleName
    )

    $module = Get-Module -ListAvailable -Name $ModuleName

    if ($null -eq $module) {
        try {
            Install-Module -Name $ModuleName -AllowClobber -Scope CurrentUser -Force -Confirm:$false
            Write-Output "$ModuleName module installed successfully."
        }
        catch {
            Write-Error "Failed to install $ModuleName module. Please run PowerShell as Administrator."
            exit
        }
    }
    else {
        Write-Output "$ModuleName module is already installed."
    }
}

function RTC-Patch-ShowDevices {
    # Get all Autopilot Devices
    $autopilotDevices = Invoke-MSGraphRequest -HttpMethod GET -Url "deviceManagement/windowsAutopilotDeviceIdentities" | Get-MSGraphAllPages

    # Filter devices where groupTag starts with "Patch"
    $filteredAutopilotDevices = $autopilotDevices | Where-Object { $_.groupTag -like "Patch*" }

    # Group the filtered devices by groupTag
    $groupedAutopilotDevices = $filteredAutopilotDevices | Group-Object -Property groupTag

    # Display each group and its devices
    foreach ($group in $groupedAutopilotDevices) {
        Write-Host "Group Tag: $($group.Name)" -ForegroundColor Green
        $group.Group | Out-GridView -Title "Devices with Group Tag: $($group.Name)"
    }

}

function RTC-Patch-ChangeAssignment {
    # Get all autopilot devices (even if more than 1000)
    $autopilotDevices = Invoke-MSGraphRequest -HttpMethod GET -Url "deviceManagement/windowsAutopilotDeviceIdentities" | Get-MSGraphAllPages

    # Ask user to select a group tag to assign
    $groupTags = @("Patch-Test", "Patch-1", "Patch-2", "Patch-3", "Patch-Last")
    $selectedGroupTag = $groupTags | Out-GridView -OutputMode Single -Title "Select the group tag you want to assign"

    # Display gridview to show devices
    $selectedAutopilotDevices = $autopilotDevices | Out-GridView -OutputMode Multiple -Title "Select Windows Autopilot entities to update"

    $selectedAutopilotDevices | ForEach-Object {

        $autopilotDevice = $PSItem


        $autopilotDevice.groupTag = $selectedGroupTag


        $requestBody =
        @"
    {
        groupTag: `"$($autopilotDevice.groupTag)`",
    }
"@
        Write-Output "Updating entity: $($autopilotDevice.id) | groupTag: $($autopilotDevice.groupTag) | orderIdentifier: $($autopilotDevice.orderIdentifier)"
        Invoke-MSGraphRequest -HttpMethod POST -Content $requestBody -Url "deviceManagement/windowsAutopilotDeviceIdentities/$($autopilotDevice.id)/UpdateDeviceProperties" 
    }

    # Invoke an autopilot service sync
    Invoke-MSGraphRequest -HttpMethod POST -Url "deviceManagement/windowsAutopilotSettings/sync"
}

# Function to display the menu and handle user input
function RTC-Patch-Display-Menu {
    $menu = @(
        "Show RTC-Patch devices & Assignments",
        "Change Assignments of RTC-Patch devices"
    )

    # Present the menu options to the user using Out-GridView
    $selectedOption = $menu | Out-GridView -Title "RTC-Patch Management Console" -OutputMode Single

    # Handle the user's choice
    switch ($selectedOption) {
        $menu[0] { RTC-Patch-ShowDevices }
        $menu[1] { RTC-Patch-ChangeAssignment }
        default { Write-Host "No valid option selected." }
    }
}

#Main Script
$moduleName = "Microsoft.Graph"
Install-ModuleIfNotPresent -ModuleName $moduleName

Disconnect-MgGraph
Connect-MSGraph
Update-MSGraphEnvironment -SchemaVersion "Beta" -Quiet
Connect-MSGraph -Quiet

# Call the display menu function
RTC-Patch-Display-Menu

Management Console for Idea 2 (Entra ID Device Extension Attributes)

It’s essential to have some kind of a management tool. In my case it’s a PowerShell script with some pre-built functions.

In detail the following functions are included:

  • Get every RTC-Patch device
  • Get current Ring assignment
  • Change Assignments of RTC-Patch devices
function Install-ModuleIfNotPresent {
    param (
        [string]$ModuleName
    )

    $module = Get-Module -ListAvailable -Name $ModuleName

    if ($null -eq $module) {
        try {
            Install-Module -Name $ModuleName -AllowClobber -Scope CurrentUser -Force -Confirm:$false
            Write-Output "$ModuleName module installed successfully."
        }
        catch {
            Write-Error "Failed to install $ModuleName module. Please run PowerShell as Administrator."
            exit
        }
    }
    else {
        Write-Output "$ModuleName module is already installed."
    }
}

function RTC-Patch-ShowDevices {
    $groupNames = @(
        "RTC-Patch-Ring-Test",
        "RTC-Patch-Ring-1",
        "RTC-Patch-Ring-2",
        "RTC-Patch-Ring-3",
        "RTC-Patch-Ring-Last"
    )
    
    Connect-MgGraph -Scopes "Group.Read.All", "Device.Read.All"

    foreach ($GroupName in $GroupNames) {
        try {
            # Get the group by name (assuming group display names are unique)
            $group = Get-MgGroup -Filter "displayName eq '$GroupName'"
            if ($group) {
                # Get members of the group
                $memberIds = Get-MgGroupMember -GroupId $group.Id -All | Select-Object Id

                $devices = @()
                foreach ($memberId in $memberIds) {
                    # Attempt to retrieve the device by the member ID
                    $device = Get-MgDevice -DeviceId $memberId.Id -ErrorAction SilentlyContinue
                    if ($device) {
                        $devices += $device
                    }
                }

                if ($devices.Count -gt 0) {
                    # Display devices in Out-GridView
                    $devices | Select-Object DisplayName, Id | Out-GridView -Title "Devices in $GroupName"
                }
                else {
                    Write-Host "No devices found in the group $GroupName" -ForegroundColor Yellow
                }
            }
            else {
                Write-Host "Group $GroupName not found" -ForegroundColor Red
            }
        }
        catch {
            Write-Host "An error occurred: $_" -ForegroundColor Red
        }
    }
}

function RTC-Patch-ChangeAssignment {
    Connect-MgGraph -Scopes "Device.ReadWrite.All"

    # Retrieve all devices
    [array]$Devices = Get-MgDevice -All

    # Define group tags for selection
    $EAvalues = @("Patch-Test", "Patch-1", "Patch-2", "Patch-3", "Patch-Last")

    # Ask user to select a value to assign
    $selectedEAvalue = $EAvalues | Out-GridView -OutputMode Single -Title "Select the value you want to assign to Entra ID extension attribute"

    # Allow user to select one or more devices from a list to update
    $selectedDevices = $Devices | Out-GridView -OutputMode Multiple -Title "Select devices to update Entra ID extension attribute"

    foreach ($Device in $selectedDevices) {
        # Prepare the JSON payload for updating the device
        $Attributes = @{
            "extensionAttributes" = @{
                "extensionAttribute5" = $selectedEAvalue
            }
        } | ConvertTo-Json

        # Update the device with the new value
        Update-MgDevice -DeviceId $Device.Id -BodyParameter $Attributes

        Write-Host ("Device {0} has been updated with Entra ID extension attribute {1}" -f $Device.DisplayName, $selectedEAvalue)
    }
}

function RTC-Patch-Display-Menu {
    $menu = @(
        "Show RTC-Patch devices & Assignments",
        "Change Assignments of RTC-Patch devices"
    )

    $selectedOption = $menu | Out-GridView -Title "RTC-Patch Management Console" -OutputMode Single

    # Handle the user's choice
    switch ($selectedOption) {
        $menu[0] { RTC-Patch-ShowDevices }
        $menu[1] { RTC-Patch-ChangeAssignment }
        default { Write-Host "No valid option selected." }
    }
}


#Main Script
$moduleName = "Microsoft.Graph"
Install-ModuleIfNotPresent -ModuleName $moduleName

# Call the display menu function
RTC-Patch-Display-Menu

Extra-Features to develop

GUI for PowerShell Management Console

It would be very user friendly to have some kind of a real application with graphical user interface. During my research I found the tool “PoshGUI” which is free. With it, it is possible to create a basic user interface like this:

POSHGUI

Feature to uninstall specific Windows Updates on specific Rings / Devices

Theoretically it would be already possible to include that feature today. You could use the cmdlet “PSWindowsUpdate” and build a script to remove a specific Windows Update with KB-Number.

But I would love to have a more intuitive solution. Maybe Microsoft will provide us with something like that soon. I would wish that!

That’s it

Now you know, there is a creative way to make Windows Autopatch available for everyone. With only 2 licenses, which almost every tenant already has. It’s no longer needed to create “exclusion-groups” for update-rings, policies, and to manage them. My solution obviously isn’t perfect and can be extended to more products, more apps and more settings within Microsoft Intune.

I can’t wait for your feedback. Let’s make this project even better together!