Last year in this article, I posted a PowerShell script to display Windows Update settings. I’ve enhanced that script to show two additional values and to optionally list all pending updates.
By default, the script will now list pending updates. Updates considered Optional (e.g. Silverlight, hardware drivers, etc.) are excluded. If you want to list pending updates, including updates considered Optional, add a $true parameter.
Update 12 November 2015
The script now lists hidden updates as well as updates that are waiting to be installed.
Update 4 August 2016
- The script now defaults to $false, i.e. it will not list pending updates unless you run it with $true. This allows it to complete very quickly by default.
- If the machine is configured to gets updates from and report status to WSUS, the relevant servers are listed.
- Windows 8/8.1/2012/2012R2 machines show task trigger information for the Regular Maintenance task (during which updates are installed).
- If you choose to list pending updates (run with $true), the full Title will now be shown, and UpdateID and RevisionNumber are included. This means each row can be up to 200 characters long. For best results, stretch your PowerShell window to make it wide enough.
Update 26 February 2018
Retrieve and display all WindowsUpdate registry values to cover Windows 10 as well.
The Results
You can run this script directly in PowerShell, but if you deploy it as a Daily Script Check in MaxRM, you’ll be able to quickly see the Windows Update settings and pending updates on any of your machines. (Tip: use the Re-run Checks option to re-run the daily checks at any time.)
Note Listing updates can take 30 seconds or longer, so if you are running this from MaxRM, set the script timeout to 60 or even 120 seconds. On slow machines, you may have to run it as a scheduled task rather than as a DSC check.
Once deployed through MaxRM, you’ll see output like this in the dashboard:
If you ran the script with a $true parameter, scroll down to see the pending updates:
Note that the pending updates are actually listed in a multi-column table (Severity, Title, etc.). MaxRM apparently removes extra white space when copying the output to the dashboard, but you can still read the output pretty easily.
The Script
And here is the script.
<# .Synopsis Print the Microsoft auto-update settings for the local computer and optionally list pending updates. Copyright (c) 2018 by MCB Systems. All rights reserved. Free for personal or commercial use. May not be sold. No warranties. Use at your own risk. .Notes Name: MCB.WindowsUpdate.ShowSettings.ps1 Author: Mark Berry, MCB Systems Created: 10/03/2014 Last Edit: 02/26/2018 Adapted from http://fixitscripts.com/problems/windows-update-settings. Also see https://technet.microsoft.com/en-us/library/ee692834.aspx http://powershellautomation.blogspot.com/2013/05/list-installed-pending-updates.html Changes: 10/10/2014 - Reformat fixed output to be narrower. 03/12/2015 - Rename script. Add RebootRequired and NoAutoRebootWithLoggedOnUsers info. Add optional list of pending updates. This takes 30+ seconds, so allow suppressing by setting new $ShowPendingUpdates param to $false or 0. 04/08/2015 - When listing updates, include optional updates (remove "BrowseOnly=0" criterion). Add BrowseOnly and IsDownloaded columns to update list. Note that Windows Defender updates include bundled updates for the base engine etc. so total size will be 400MB+. Most of the "sub-updates" show IsInstalled=True but are still included in total size. Getting true size would require looping through bundled updates, which I won't try to do now. 04/17/2015 - Edit note about optional updates. Add note about Microsoft anti-virus update size. 11/12/2015 - Also list hidden updates if ShowPendingUpdates = $true. 02/17/2016 - Change default for $ShowPendingUpdates to $false. This lets script execute quickly when run with no params. - For Windows 8/8.1/2012/2012R2, show start trigger of the Regular Maintenance task, which controls when updates are installed. No longer used in Windows 10. 08/04/2016 - When displaying pending updates, use Format-Table -AutoSize to show entire Title. Add a column for UpdateID with RevisionNumber. Output may now be up to 200 characters wide--you may want to copy to a text editor without line wrapping. 08/04/2016 - If the machine is configured to get updates from WSUS, identify the WUServer and WUStatusServer. Otherwise indicated that WSUS is not configured. 02/26/2018 - Retrieve and display all WindowsUpdate registy values to cover Windows 10 as well. #> param( [Parameter(Mandatory = $false, Position = 0, ValueFromPipelineByPropertyName = $true)] [Boolean]$ShowPendingUpdates=$false, [Parameter(Mandatory = $false, Position = 1, ValueFromPipelineByPropertyName = $true)] [String]$LogFile="" ) [Boolean]$ErrFound = $false function ListRegKeyValues { param( [Parameter(Mandatory=$true)] [string]$path) "Hive: $path`n" $RegKey = (Get-ItemProperty $path) $RegKey.PSObject.Properties | ForEach-Object { $Name = $_.Name $Value = $_.Value switch ($Name) { # Don't print the PowerShell properties that are apparently present in every PSObject "PSPath" { break } "PSParentPath" { break } "PSChildName" { break } "PSDrive" { break } "PSProvider" { break } default { Write-Host $Name ': ' $Value } } } } "Computer Name: " + $env:COMPUTERNAME "" "Microsoft AutoUpdate settings (from the Microsoft.Update COM object)" "--------------------------------------------------------------------" try { $objAutoUpdateSettings = (New-Object -ComObject "Microsoft.Update.AutoUpdate").Settings $objSysInfo = New-Object -ComObject "Microsoft.Update.SystemInfo" # See https://stackoverflow.com/a/32253197/550712 for the Out-String.Trim() trick to trim blank lines ($objAutoUpdateSettings | Out-String).Trim() "Reboot required : " + $objSysInfo.RebootRequired "" "Registry values (set by Group Policy or manually)" Write-Host -NoNewLine ("-------------------------------------------------`n") $path = "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate" ListRegKeyValues $path "" $path = "HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate\AU" ListRegKeyValues $path # WSUS server info, if available # Reference: https://technet.microsoft.com/en-us/library/dd939844(v=ws.10).aspx "" Write-Host -NoNewLine ("WSUS Server : ") try { # If Get-ItemProperty fails, value is not in registry. Do not fail entire script. # "-ErrorAction Stop" forces it to catch even a non-terminating error. $output = Get-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate -Name WUServer -ErrorAction Stop $output.WUServer } catch { "WSUS not configured. The machine must be contacting Windows Update directly." } Write-Host -NoNewLine ("WSUS Status Server : ") try { $output = Get-ItemProperty -Path HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate -Name WUStatusServer -ErrorAction Stop $output.WUStatusServer } catch { "WSUS not configured. The machine must be contacting Windows Update directly." } # Static info on the meaning of various Settings. "" "Key to Notification Values" "--------------------------" "NotificationLevel:" "1 - Never check for updates" "2 - Check for updates but let me choose whether to download and install them" "3 - Download updates but let me choose whether to install them" "4 - Install updates automatically" "" "ScheduledInstallationDay:" "0 - Every day" "1-7 - Sunday through Saturday" "" "For Windows 10, you should also see 'ScheduledInstallEveryWeek = 1 and/or ScheduledInstallFirstWeek = 1, etc." "" # For Windows 8 and later, show start trigger of the Regular Maintenance task # Per http://stackoverflow.com/a/26003354/550712 .NET OS Version is inaccurate on an # upgraded Windows 8.1 machine, so use Get-CimInstance if available, else fall back to .NET. # Cast as [Version] to allow accurate comparisons even when Version.Major is two digits (Windows 10). [Version]$OSVersion = $null try { $OSVersion = (Get-CimInstance Win32_OperatingSystem).Version } catch { # Get-CimInstance requires PowerShell 3. Fall back to .NET if Get-CimInstance doesn't work. $OSVersion = [System.Environment]::OSVersion.Version } # List of Windows versions: http://www.robvanderwoude.com/ver.php if ( ($OSVersion -ge [Version]"6.2.9200") ) { "Windows 8 and Later Scheduled Maintenance, If Any" "-------------------------------------------------" try { """\Microsoft\Windows\TaskScheduler\Regular Maintenance"" task trigger:" $task = Get-ScheduledTask -TaskName "Regular Maintenance" -TaskPath "\Microsoft\Windows\TaskScheduler\" -ErrorAction Stop $task.Triggers } catch { "Error: Could not retrieve \Microsoft\Windows\TaskScheduler\Regular Maintenance task" "" } # To change, use # $TaskTime = New-ScheduledTaskTrigger -At 12:00 -Daily # Set-ScheduledTask -TaskName "Regular Maintenance" -TaskPath "\Microsoft\Windows\TaskScheduler\" -Trigger $TaskTime -ErrorAction Stop } "Pending Software Updates including Hidden Updates" "-------------------------------------------------" if ($ShowPendingUpdates) { #Get All Assigned updates in $SearchResult. $UpdateSession = New-Object -ComObject Microsoft.Update.Session $UpdateSearcher = $UpdateSession.CreateUpdateSearcher() # Available search criteria: https://msdn.microsoft.com/en-us/library/windows/desktop/aa386526%28v=vs.85%29.aspx # "BrowseOnly=0" omits updates that are considered "Optional" (e.g. Silverlight, hardware drivers). # As of 4/8/2015, omit "BrowseOnly=0" so we'll see all available updates. # Include "IsAssigned=1" to only see updates intended for deployment by Automatic Updates: # $SearchResult = $UpdateSearcher.Search("IsAssigned=1 and IsHidden=0 and IsInstalled=0") # Omit "IsAssigned=1" to also see Recommended updates: $SearchResult = $UpdateSearcher.Search("IsInstalled=0") #Extract Results for type of updates that are needed. For to be arrays so we can .count them. [Object[]] $Critical = $SearchResult.updates | where { $_.MsrcSeverity -eq "Critical" } [Object[]] $Important = $SearchResult.updates | where { $_.MsrcSeverity -eq "Important" } [Object[]] $Moderate = $SearchResult.updates | where { $_.MsrcSeverity -eq "Moderate" } [Object[]] $Low = $SearchResult.updates | where { $_.MsrcSeverity -eq "Low" } [Object[]] $Unspecified = $SearchResult.updates | where { $_.MsrcSeverity -eq "Unspecified" } [Object[]] $Other = $SearchResult.updates | where { $_.MsrcSeverity -eq $null } #Write Results "Critical : $($Critical.count)" "Important : $($Important.count)" "Moderate : $($Moderate.count)" "Low : $($Low.count)" "Unspecified : $($Unspecified.count)" "Other : $($Other.count)" "Total : $($SearchResult.updates.count)" "" "Notes: ""BrowseOnly"" updates are considered optional." " Microsoft anti-virus updates include sub-updates" " that are already installed, so size is inaccurate." # "If" statement in Expression: # http://blogs.technet.com/b/josebda/archive/2014/04/19/powershell-tips-for-building-objects-with-custom-properties-and-special-formatting.aspx # Formatting number as MB: https://technet.microsoft.com/en-us/library/ff730948.aspx # Available update properties (IUpdate interface): https://msdn.microsoft.com/en-us/library/windows/desktop/aa386099(v=vs.85).aspx # Use Out-String to keep AutoSize from truncating columns based on screen size: # https://poshoholic.com/2010/11/11/powershell-quick-tip-creating-wide-tables-with-powershell/ "" "Ready to Install" "----------------" $NotHiddenUpdates = $SearchResult.updates | Where-Object {$_.IsHidden -eq $false} If ($NotHiddenUpdates -eq $null) { "None" } else { $NotHiddenUpdates | Sort-Object MsrcSeverity, Title | ` Format-Table -AutoSize @{Expression={if ($_.MsrcSeverity -eq $null) {"Other"} else {$_.MsrcSeverity} };Label="Severity"}, ` @{Expression={$_.Title};Label="Title"}, ` @{Expression={"{" + $_.Identity.UpdateID + "}." + $_.Identity.RevisionNumber};Label="UpdateID and RevisionNumber"}, ` @{Expression={$_.BrowseOnly};Label="BrowseOnly"}, ` @{Expression={$_.IsDownloaded};Label="IsDownloaded"}, ` @{Expression={"{0:N1} MB" -f ($_.MaxDownloadSize / 1MB) };Label="MaxDownload";align="right"} | ` Out-String -Width 200 } "" "Hidden Updates" "--------------" $HiddenUpdates = $SearchResult.updates | Where-Object {$_.IsHidden -eq $true} If ($HiddenUpdates -eq $null) { "None" } else { $HiddenUpdates | Sort-Object MsrcSeverity, Title | ` Format-Table -AutoSize @{Expression={if ($_.MsrcSeverity -eq $null) {"Other"} else {$_.MsrcSeverity} };Label="Severity"}, ` @{Expression={$_.Title};Label="Title"}, ` @{Expression={"{" + $_.Identity.UpdateID + "}." + $_.Identity.RevisionNumber};Label="UpdateID and RevisionNumber"}, ` @{Expression={$_.BrowseOnly};Label="BrowseOnly"}, ` @{Expression={$_.IsDownloaded};Label="IsDownloaded"}, ` @{Expression={"{0:N1} MB" -f ($_.MaxDownloadSize / 1MB) };Label="MaxDownload";align="right"} | ` Out-String -Width 200 } } else { "The ShowPendingUpdates parameter is `$false, so pending updates not listed." "" } # if $ShowPendingUpdates "Script execution succeeded" $ExitCode = 0 } # try catch { "" $error[0] "" "Script execution failed" $ExitCode = 1001 # Cause script to report failure in MaxFocus RM dashboard } "" "Local Machine Time: " + (Get-Date -Format G) "Exit Code: " + $ExitCode Exit $ExitCode
Pingback: PowerShell Script to Change Windows Update Settings | MCB Systems
Pingback: Print Detailed Windows Update Information | MCB Systems
This does not work correctly for any domain joined machines. It does not show any of the GPO based settings.
Brachus, thanks for the feedback. What OS are you using? The results should be accurate if not complete for domain machines. For example, if your GPO sets Windows Update to “2 – Notify…”, you should see that in this script, but you won’t see that the user is blocked from changing that setting. What exactly where you hoping to see?
how to you run this for a list of computers,
I keep getting pipeline failures.
David, the script only shows information about the machine where it is run. I use a monitoring tool to deploy it on multiple machines.
Pingback: Windows 10 Does Not Show or Install Optional Updates | MCB Systems
Hi Mark!
Your script reference a $LogFile parameter, but that is not really used.
Do you have a version that includes its usage?
It would be very usefull.
Thanks in advance for your attention.
Andres – you’re right, I don’t use the $LogFile parameter.. That parameter is only used when the script is deployed and called from MaxRM. Per https://dashboard.systemmonitor.us/dashboard/helpcontents/script_guide_parse.htm, “When the Agent runs the script two arguments are always appended to the command line (for debug) and the -logfile logname can be ignored.” Put another way, if you DON’T have the $LogFile param and you call it from MaxRM, it will pass the unexpected param and the script will fail.
Mark,
Nice piece of code. I did a little work on it to fit it to my needs to output a single string to return to another program. I thought you might find some of the changes useful.
Thanks Bruce. I wrapped the code in a “pre” tag. Not pretty in the comment but I think if anyone wants to cut and paste, it will retain spacing.