SCRIPT LIBRARY · POWERSHELL
Opting Devices In to Microsoft Update with an SCCM Package
Register the Microsoft Update service through the Windows Update Agent's own COM API, so devices pick up Office and other Microsoft product updates, not just Windows.
- What it does
- Registers (or removes) the Microsoft Update service with the Windows Update Agent, the same thing the "Receive updates for other Microsoft products" toggle does, and reports the result with an exit code ConfigMgr understands.
- Requires
- Windows PowerShell 5.1 (PowerShell 7 works too)
- No modules. It talks to the Windows Update Agent's COM API directly.
- Permissions
- Local administrator. As a ConfigMgr program it runs as SYSTEM, which is exactly what you want.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and dry-run with a mocked ServiceManager COM object in PowerShell 7.4
Part 4 of the thread Windows Update, untangled
Out of the box, Windows Update only offers Windows. The updates for Office, the Visual C++ runtimes, SQL Server and the rest come through Microsoft Update, which is a separate service the Windows Update Agent has to be told about. On one machine that's a toggle in Settings. On three hundred, nobody's clicking anything.
Under that toggle is a COM object, Microsoft.Update.ServiceManager, and one method call. This script makes that call, checks whether it's already been done, and hands back an object saying what happened. Wrap it in a ConfigMgr package and every targeted device gets opted in.
The original version of this post was almost all packaging boilerplate around a single line: Add-WUServiceManager from the PSWindowsUpdate module, which isn't on a stock Windows install. Its service GUID was also one character short, so on most machines it quietly did nothing. This rewrite drops the module dependency and gets the GUID right.
<#
.SYNOPSIS
Opts a Windows device in to Microsoft Update (or back out of it).
.DESCRIPTION
Uses the Microsoft.Update.ServiceManager COM object, the same API behind the
"Receive updates for other Microsoft products" toggle in Settings. Once the
Microsoft Update service is registered, Windows Update offers fixes for Office,
SQL Server, Visual C++ runtimes and other Microsoft products, not just Windows.
Returns an object describing the service afterwards and sets exit code 0 on
success or 1 on failure, so it drops straight into a ConfigMgr program.
.PARAMETER ServiceId
The update service to register. Defaults to Microsoft Update.
.PARAMETER Remove
Unregister the service instead of registering it.
.PARAMETER SkipAutomaticUpdates
Register the service, but don't make it part of scheduled Automatic Updates scans.
.EXAMPLE
.\Register-MicrosoftUpdateService.ps1 -WhatIf
.EXAMPLE
.\Register-MicrosoftUpdateService.ps1 -Remove
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[ValidatePattern('^[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}$')]
[string]$ServiceId = '7971f918-a847-4430-9279-4a52d1ef18d2',
[switch]$Remove,
[switch]$SkipAutomaticUpdates
)
# AddService2 flags: allow pending registration (1), allow online registration (2), register with AU (4).
$flags = 1 -bor 2
if (-not $SkipAutomaticUpdates) { $flags = $flags -bor 4 }
$stateNames = @{ 1 = 'NotRegistered'; 2 = 'RegistrationPending'; 3 = 'Registered' }
function Get-ServiceState {
param($Manager, [string]$Id)
$svc = @($Manager.Services) | Where-Object { $_.ServiceID -eq $Id } | Select-Object -First 1
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
ServiceId = $Id
Name = if ($svc) { $svc.Name } else { $null }
Registered = [bool]$svc
RegisteredWithAU = if ($svc) { [bool]$svc.IsRegisteredWithAU } else { $false }
IsDefaultAUService = if ($svc) { [bool]$svc.IsDefaultAUService } else { $false }
Result = $null
}
}
try {
$manager = New-Object -ComObject Microsoft.Update.ServiceManager
$manager.ClientApplicationID = 'Register-MicrosoftUpdateService'
}
catch {
Write-Error "Couldn't create the Windows Update ServiceManager COM object: $($_.Exception.Message)"
exit 1
}
$before = Get-ServiceState -Manager $manager -Id $ServiceId
Write-Verbose ("Before: Registered={0}, RegisteredWithAU={1}" -f $before.Registered, $before.RegisteredWithAU)
$exitCode = 0
$result = 'NoChange'
try {
if ($Remove) {
if (-not $before.Registered) {
Write-Verbose 'Service is not registered. Nothing to remove.'
}
elseif ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Remove update service $ServiceId")) {
$manager.RemoveService($ServiceId)
$result = 'Removed'
}
else { $result = 'WhatIf' }
}
else {
$alreadyDone = $before.Registered -and ($SkipAutomaticUpdates -or $before.RegisteredWithAU)
if ($alreadyDone) {
Write-Verbose 'Service is already registered the way you asked. Nothing to do.'
}
elseif ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, "Register update service $ServiceId (flags $flags)")) {
# Third argument is the authorization cab path. Microsoft Update doesn't need one.
$registration = $manager.AddService2($ServiceId, $flags, '')
$result = $stateNames[[int]$registration.RegistrationState]
if (-not $result) { $result = "State$($registration.RegistrationState)" }
}
else { $result = 'WhatIf' }
}
}
catch {
Write-Error "Update service change failed: $($_.Exception.Message)"
$result = 'Failed'
$exitCode = 1
}
$after = Get-ServiceState -Manager $manager -Id $ServiceId
$after.Result = $result
$after
# A normal finish returns exit code 0. Only a failure needs to say otherwise.
if ($exitCode) { exit $exitCode }
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-ServiceId | string | 7971f918-a847-4430-9279-4a52d1ef18d2 | The update service to register. The default is Microsoft Update, and you'll rarely want anything else. |
-Remove | switch | — | Unregister the service instead. Handy for backing the change out. |
-SkipAutomaticUpdates | switch | — | Register the service without adding it to scheduled Automatic Updates scans. Manual scans still see it. |
Run it
See what it would do first. Nothing changes.
.\Register-MicrosoftUpdateService.ps1 -WhatIfOpt this machine in, and show the before and after state.
.\Register-MicrosoftUpdateService.ps1 -VerboseThe command line for a ConfigMgr program.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Register-MicrosoftUpdateService.ps1Changed your mind? Take it back out.
.\Register-MicrosoftUpdateService.ps1 -RemoveWhat you'll see
ComputerName : PC-0142
ServiceId : 7971f918-a847-4430-9279-4a52d1ef18d2
Name : Microsoft Update
Registered : True
RegisteredWithAU : True
IsDefaultAUService : False
Result : Registered
How it works
It's a short script, because the Windows Update Agent does the real work.
- Grab the ServiceManager.
New-Object -ComObject Microsoft.Update.ServiceManagergives you the same object the Settings app uses. If that fails, the Windows Update Agent itself is broken and the script exits with code 1. - Check before changing. It walks the
Servicescollection looking for the Microsoft Update GUID. If it's already registered, and registered with Automatic Updates, it does nothing and saysNoChange. Running it twice is safe. - Register with the right flags.
AddService2takes the service ID, a flag value and an authorization cab path (empty for Microsoft Update). The flags add up: 1 allows a pending registration, 2 allows online registration, 4 registers the service with Automatic Updates. The default is 7, all three. - Report and exit. You get one object with the state after the change. Success exits 0, failure exits 1, so the deployment status in the console actually means something.
To package it, drop the script in a source folder and create a package and program. This is the whole thing, with placeholders for your own site code and paths:
Import-Module "$env:SMS_ADMIN_UI_PATH\..\ConfigurationManager.psd1"
Set-Location 'ABC:'
New-CMPackage -Name 'Register Microsoft Update' -Path '\\sccm01\Sources\Scripts\Register-MicrosoftUpdateService'
New-CMProgram -PackageName 'Register Microsoft Update' -StandardProgramName 'Register' -CommandLine 'powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Register-MicrosoftUpdateService.ps1' -ProgramRunType WhetherOrNotUserIsLoggedOn
Start-CMContentDistribution -PackageName 'Register Microsoft Update' -DistributionPointGroupName 'All Distribution Points'
One thing the old version did that I'd skip: it deleted any existing package with the same name before creating a new one. That throws away the deployment history and breaks any deployment pointing at the old package ID. If the package exists, update its content instead.
Take it further
- List every registered service.
(New-Object -ComObject Microsoft.Update.ServiceManager).Services | Select-Object Name, ServiceID, IsDefaultAUServiceis a quick sanity check on any machine. - Make it a compliance check. Split the "is it registered?" part into a configuration item discovery script and use this script as the remediation, so drift gets fixed on its own.
- Pair it with the update-source check. If you're not sure whether a device talks to WSUS, Windows Update for Business or plain Windows Update, figure that out first. It decides whether this script matters at all.
Things that'll trip you up
- WSUS and ConfigMgr devices don't need this. If a device scans against WSUS or a ConfigMgr software update point, Office and friends come from there, as long as you've selected those products in the SUP. Registering Microsoft Update on those machines mostly just adds noise.
- RegistrationPending isn't a failure. Online registration needs to reach Microsoft. If it can't, the agent queues the registration and finishes it on its next successful contact. Check again after the next scan.
- On Intune, use policy instead. The Update policy CSP has an AllowMUUpdateService setting that does the same job and keeps doing it. A script is a one-time nudge; policy is the thing that sticks.
- Copy the GUID exactly. It's 7971f918-a847-4430-9279-4a52d1ef18d2, 36 characters with the dashes. One missing digit and AddService2 fails, which is how the old version of this post went wrong.