Windows Server Update Services (WSUS) plays a critical role in centralized patch management for Windows environments. For IT professionals and managed service providers (MSPs), understanding whether WSUS is enabled—and if so, how it’s configured—is essential for maintaining system compliance, patch consistency, and operational efficiency. This PowerShell script is a comprehensive tool for auditing WSUS settings, particularly helpful in environments where settings may be controlled via Group Policy Objects (GPOs).
Background
WSUS allows administrators to manage the distribution of updates released through Microsoft Update. However, configuration visibility can be opaque—especially in environments with Group Policies layered across organizational units. Manually inspecting registry keys and GPO paths is both inefficient and error-prone.
This PowerShell script was designed to address this complexity. It not only checks whether WSUS is enabled but also determines whether the configuration originates from registry values or GPOs. Additionally, it offers the option to record the results in a NinjaOne custom field, enabling asset-level visibility in remote monitoring and management (RMM) tools.
The Script
#Requires -Version 5.1
<#
.SYNOPSIS
Determines if Windows Server Update Services (WSUS) settings are configured in the registry and identifies if they are managed via Group Policy (GPO). You can also write the results to a text custom field.
.DESCRIPTION
Determines if Windows Server Update Services (WSUS) settings are configured in the registry and identifies if they are managed via Group Policy (GPO). You can also write the results to a text custom field.
By using this script, you indicate your acceptance of the following legal terms as well as our Terms of Use at https://www.ninjaone.com/terms-of-use.
Ownership Rights: NinjaOne owns and will continue to own all right, title, and interest in and to the script (including the copyright). NinjaOne is giving you a limited license to use the script in accordance with these legal terms.
Use Limitation: You may only use the script for your legitimate personal or internal business purposes, and you may not share the script with another party.
Republication Prohibition: Under no circumstances are you permitted to re-publish the script in any script library or website belonging to or under the control of any other software provider.
Warranty Disclaimer: The script is provided “as is” and “as available”, without warranty of any kind. NinjaOne makes no promise or guarantee that the script will be free from defects or that it will meet your specific needs or expectations.
Assumption of Risk: Your use of the script is at your own risk. You acknowledge that there are certain inherent risks in using the script, and you understand and assume each of those risks.
Waiver and Release: You will not hold NinjaOne responsible for any adverse or unintended consequences resulting from your use of the script, and you waive any legal or equitable rights or remedies you may have against NinjaOne relating to your use of the script.
EULA: If you are a NinjaOne customer, your use of the script is subject to the End User License Agreement applicable to you (EULA).
.PARAMETER CustomFieldName
The name of the custom field to set with WSUS settings information.
.EXAMPLE
(No Parameters)
[Info] Updating group policies...
[Info] Group policy update completed successfully.
[Info] Checking the registry for WSUS settings...
[Info] WSUS Update Server detected in the registry: https://test.local.another.sub.domain/test/testagain:8562
[Info] WSUS Statistics Server detected in the registry: https://test.local.another.sub.domain/test/testagain:8562
[Info] Checking for GPOs that configure WSUS settings...
[Info] Found GPOs that affect WSUS settings.
### Active WSUS settings: ###
WSUS Settings Source : GPO
WSUS Status : Enabled
GPO Display Name : A_WSUS_Settings
Update Server : https://test.local.another.sub.domain/test/testagain:8562
Statistics Server : https://test.local.another.sub.domain/test/testagain:8562
.EXAMPLE
-CustomFieldName "WSUSSettings"
[Info] Updating group policies...
[Info] Group policy update completed successfully.
[Info] Checking the registry for WSUS settings...
[Info] WSUS Update Server detected in the registry: https://test.local.another.sub.domain/test/testagain:8562
[Info] WSUS Statistics Server detected in the registry: https://test.local.another.sub.domain/test/testagain:8562
[Info] Checking for GPOs that configure WSUS settings...
[Info] Found GPOs that affect WSUS settings.
### Active WSUS settings: ###
WSUS Settings Source : GPO
WSUS Status : Enabled
GPO Display Name : A_WSUS_Settings
Update Server : https://test.local.another.sub.domain/test/testagain:8562
Statistics Server : https://test.local.another.sub.domain/test/testagain:8562
[Info] Setting the custom field 'WSUSSettings' with the value:
WSUS Status: Enabled | Update and Statistics Server: https://test.local.another.sub.domain/test/testagain:8562 | GPO Name: A_WSUS_Settings
[Info] Successfully set the custom field 'WSUSSettings'.
.NOTES
Minimum OS Architecture Supported: Windows 10, Windows Server 2016
Release Notes: Initial release
#>
[CmdletBinding()]
param (
[string]$CustomFieldName
)
begin {
# Import custom field from script variable
if ($env:textCustomFieldName) { $CustomFieldName = $env:textCustomFieldName }
# Validate the custom field name if provided
if ($CustomFieldName) {
# Trim the custom field name to remove any leading or trailing whitespace
$CustomFieldName = $CustomFieldName.Trim()
# Error if the custom field name is empty
if ([string]::IsNullOrWhiteSpace($CustomFieldName)) {
Write-Host -Object "[Error] The value for 'Text Custom Field Name' cannot be empty."
Write-Host -Object "[Error] Please provide a valid text custom field name to save the results, or leave it blank."
exit 1
}
# Validate that the field name contains only alphanumeric characters
if ($CustomFieldName -match "[^0-9A-Z]") {
Write-Host -Object "[Error] The 'Text Custom Field Name' of '$CustomFieldName' is invalid as it contains invalid characters."
Write-Host -Object "[Error] Please provide a valid text custom field name to save the results, or leave it blank."
Write-Host -Object "[Error] https://ninjarmm.zendesk.com/hc/en-us/articles/360060920631-Custom-Field-Setup"
exit 1
}
}
# Function to set a custom field in NinjaOne
function Set-CustomField {
[CmdletBinding()]
Param(
[Parameter(Mandatory = $True)]
[String]$Name,
[Parameter(Mandatory = $True, ValueFromPipeline = $True)]
$Value,
[Parameter()]
[String]$Type,
[Parameter()]
[String]$DocumentName,
[Parameter()]
[Switch]$Piped
)
if ($Type -eq "Date Time") { $Type = "DateTime" }
if ($Type -match "[-]") { $Type = $Type -replace '-' }
if ($Type -match "[/]") { $Type = $Type -replace '/' }
# Remove the non-breaking space character
if ($Type -eq "WYSIWYG") {
$Value = $Value -replace ' ', ' '
}
if ($Type -eq "DateTime" -or $Type -eq "Date") {
$Type = "Date or Date Time"
}
# Measure the number of characters in the provided value
$Characters = $Value | ConvertTo-Json | Measure-Object -Character | Select-Object -ExpandProperty Characters
# Throw an error if the value exceeds the character limit of 200,000 characters
if ($Piped -and $Characters -ge 200000) {
throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 200,000 characters.")
}
if (!$Piped -and $Characters -ge 45000) {
throw [System.ArgumentOutOfRangeException]::New("Character limit exceeded: the value is greater than or equal to 45,000 characters.")
}
# Initialize a hashtable for additional documentation parameters
$DocumentationParams = @{}
# If a document name is provided, add it to the documentation parameters
if ($DocumentName) { $DocumentationParams["DocumentName"] = $DocumentName }
# Define a list of valid field types
$ValidFields = "Checkbox", "Date", "Date or Date Time", "DateTime", "Decimal", "Dropdown", "Email", "Integer", "IP Address", "MultiLine",
"MultiSelect", "Phone", "Secure", "Text", "Time", "URL", "WYSIWYG"
# Warn the user if the provided type is not valid
if ($Type -and $ValidFields -notcontains $Type) { Write-Warning "$Type is an invalid type. Please check here for valid types: https://ninjarmm.zendesk.com/hc/en-us/articles/16973443979789-Command-Line-Interface-CLI-Supported-Fields-and-Functionality" }
# Define types that require options to be retrieved
$NeedsOptions = "Dropdown", "MultiSelect"
# If the property is being set in a document or field and the type needs options, retrieve them
if ($DocumentName) {
if ($NeedsOptions -contains $Type) {
$NinjaPropertyOptions = Ninja-Property-Docs-Options -AttributeName $Name @DocumentationParams 2>&1
}
}
else {
if ($NeedsOptions -contains $Type) {
$NinjaPropertyOptions = Ninja-Property-Options -Name $Name 2>&1
}
}
# Throw an error if there was an issue retrieving the property options
if ($NinjaPropertyOptions.Exception) { throw $NinjaPropertyOptions }
# Process the property value based on its type
switch ($Type) {
"Checkbox" {
# Convert the value to a boolean for Checkbox type
$NinjaValue = [System.Convert]::ToBoolean($Value)
}
"Date or Date Time" {
# Convert the value to a Unix timestamp for Date or Date Time type
$Date = (Get-Date $Value).ToUniversalTime()
$TimeSpan = New-TimeSpan (Get-Date "1970-01-01 00:00:00") $Date
[long]$NinjaValue = $TimeSpan.TotalSeconds
}
"Dropdown" {
# Convert the dropdown value to its corresponding GUID
$Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
$Selection = $Options | Where-Object { $_.Name -eq $Value } | Select-Object -ExpandProperty GUID
# Throw an error if the value is not present in the dropdown options
if (!($Selection)) {
throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.")
}
$NinjaValue = $Selection
}
"MultiSelect" {
$Options = $NinjaPropertyOptions -replace '=', ',' | ConvertFrom-Csv -Header "GUID", "Name"
$Selections = New-Object System.Collections.Generic.List[String]
if ($Value -match "[,]") {
$Value = $Value -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ }
}
$Value | ForEach-Object {
$GivenValue = $_
$Selection = $Options | Where-Object { $_.Name -eq $GivenValue } | Select-Object -ExpandProperty GUID
# Throw an error if the value is not present in the dropdown options
if (!($Selection)) {
throw [System.ArgumentOutOfRangeException]::New("Value is not present in dropdown options.")
}
$Selections.Add($Selection)
}
$NinjaValue = $Selections -join ","
}
"Time" {
# Convert the value to a Unix timestamp for Date or Date Time type
$LocalTime = (Get-Date $Value)
$LocalTimeZone = [TimeZoneInfo]::Local
$UtcTime = [TimeZoneInfo]::ConvertTimeToUtc($LocalTime, $LocalTimeZone)
[long]$NinjaValue = ($UtcTime.TimeOfDay).TotalSeconds
}
default {
# For other types, use the value as is
$NinjaValue = $Value
}
}
# Set the property value in the document if a document name is provided
if ($DocumentName) {
$CustomField = Ninja-Property-Docs-Set -AttributeName $Name -AttributeValue $NinjaValue @DocumentationParams 2>&1
}
else {
try {
# Otherwise, set the standard property value
if ($Piped) {
$CustomField = $NinjaValue | Ninja-Property-Set-Piped -Name $Name 2>&1
}
else {
$CustomField = Ninja-Property-Set -Name $Name -Value $NinjaValue 2>&1
}
}
catch {
throw $_.Exception.Message
}
}
# Throw an error if setting the property failed
if ($CustomField.Exception) {
throw $CustomField
}
}
# Function to test if a device is domain-joined
function Test-IsDomainJoined {
# Check the PowerShell version to determine the appropriate cmdlet to use
try {
if ($PSVersionTable.PSVersion.Major -lt 3) {
return $(Get-WmiObject -Class Win32_ComputerSystem).PartOfDomain
}
else {
return $(Get-CimInstance -Class Win32_ComputerSystem).PartOfDomain
}
}
catch {
Write-Host -Object "[Error] Unable to validate whether or not this device is a part of a domain."
Write-Host -Object "[Error] $($_.Exception.Message)"
exit 1
}
}
# Function to test if the current device is a server or domain controller
function Test-IsServer {
[CmdletBinding()]
param()
# Determine the method to retrieve the operating system information based on PowerShell version
$OS = if ($PSVersionTable.PSVersion.Major -lt 3) {
Get-WmiObject -Class Win32_OperatingSystem
}
else {
Get-CimInstance -ClassName Win32_OperatingSystem
}
# Check if the ProductType is "2", which indicates that the system is a domain controller or is a server
if ($OS.ProductType -eq "2" -or $OS.ProductType -eq "3") {
return $true
}
}
# Function to test if the current session is running with Administrator privileges
function Test-IsElevated {
[CmdletBinding()]
param ()
# Get the current Windows identity of the user running the script
$id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
# Create a WindowsPrincipal object based on the current identity
$p = New-Object System.Security.Principal.WindowsPrincipal($id)
# Check if the current user is in the Administrator role
# The function returns $True if the user has administrative privileges, $False otherwise
# 544 is the value for the Built In Administrators role
# Reference: https://learn.microsoft.com/en-us/dotnet/api/system.security.principal.windowsbuiltinrole
$p.IsInRole([System.Security.Principal.WindowsBuiltInRole]'544')
}
}
process {
# Attempt to determine if the current session is running with Administrator privileges.
try {
$IsElevated = Test-IsElevated -ErrorAction Stop
} catch {
# Output error if unable to determine elevation status
Write-Host -Object "[Error] $($_.Exception.Message)"
Write-Host -Object "[Error] Unable to determine if the account '$env:Username' is running with Administrator privileges."
exit 1
}
# Exit if not running as an administrator
if (!$IsElevated) {
Write-Host -Object "[Error] Access Denied: Please run with Administrator privileges."
exit 1
}
# Check if the device is domain-joined
$IsDomainJoined = Test-IsDomainJoined
# If domain-joined, do a group policy update
if ($IsDomainJoined) {
# Run gpupdate to ensure policy is refreshed
Write-Host -Object "[Info] Updating group policies..."
# Generate unique log file names for capturing the stdout and stderr of the 'gpupdate.exe' process.
$StandardOutLog = "$env:TEMP\$(Get-Random)_gpupdate_stdout.log"
try {
# Start the group policy update process
$gpupdateProcess = Start-Process -FilePath "$env:SystemRoot\System32\gpupdate.exe" -ArgumentList "/force" -Wait -NoNewWindow -PassThru -RedirectStandardOutput $StandardOutLog -ErrorAction Stop
} catch {
# If the 'gpupdate.exe' process fails to start, output an error message
Write-Host -Object "[Error] $($_.Exception.Message)"
Write-Host -Object "[Error] Failed to start '$env:SystemRoot\System32\gpupdate.exe'."
}
# If the exit code is non-zero (indicating an error occurred), display an error message
if ($gpupdateProcess.ExitCode -ne 0) {
Write-Host -Object "[Error] Failed to update group policy, exit code: $($gpupdateProcess.ExitCode)"
# Check if the standard output log file exists.
if (Test-Path -Path $StandardOutLog -ErrorAction SilentlyContinue) {
# Retrieve the contents of the error log
$errorContent = Get-Content -Path $StandardOutLog -ErrorAction SilentlyContinue
if ($ErrorContent) {
# Remove blank lines and the header of the log
$errorContent = $errorContent | Where-Object { $_ } | Select-Object -Skip 1 | Out-String
# Add [Error] prefix to the beginning
$errorContent = $errorContent.Trim() -replace "^", "[Error] "
# Display the error content to the user
$errorContent | Out-Host
}
try {
# Attempt to delete the stdout log file after displaying its contents.
Remove-Item -Path $StandardOutLog -ErrorAction Stop
} catch {
Write-Host -Object "[Error] Failed to remove standard output log at '$StandardOutLog'"
}
}
Write-Host -Object ""
Write-Host -Object "[Warning] Failed to update group policy. Results may not reflect the latest group policy settings."
} else {
Write-Host -Object "[Info] Group policy update completed successfully."
}
}
# Initialize exit code and output object
$ExitCode = 0
$ActiveWSUSSettings = [PSCustomObject]::new()
# Define registry path for WSUS settings
$wsusRegPath = 'HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate'
# Check registry for WSUS settings
if ((Test-Path $wsusRegPath)) {
Write-Host -Object "`n[Info] Checking the registry for WSUS settings..."
try {
$useWUServer = (Get-ItemProperty -Path "$wsusRegPath\AU" -ErrorAction Stop).UseWUServer
} catch {
Write-Host -Object "`n[Error] Error retrieving WSUS settings from the registry."
Write-Host -Object "[Error] $($_.Exception.Message)"
exit 1
}
# Add registry as the source to the active settings object
$ActiveWSUSSettings | Add-Member -MemberType NoteProperty -Name "WSUS Settings Source" -Value "Registry"
# If the GPO setting is configured, the UseWUServer regkey will be present and populated
switch ($useWUServer) {
0 {
$checkForGPOs = $true
$WSUSStatus = "Disabled"
}
1 {
$checkForGPOs = $true
$WSUSStatus = "Enabled"
}
default {
# If the UseWUServer regkey is not present or is a different value, it means WSUS is not configured via GPO, so we can skip checking for GPOs
$checkForGPOs = $false
$WSUSStatus = "Not Configured"
Write-Host -Object "[Info] WSUS is not enabled on this device."
}
}
# Add the WSUS status to the active settings object
$ActiveWSUSSettings | Add-Member -MemberType NoteProperty -Name "WSUS Status" -Value $WSUSStatus
# Retrieve the WSUS server and statistics server from the registry
$wsusServerReg = Get-ItemProperty -Path "$wsusRegPath" -Name WUServer, WUStatusServer -ErrorAction SilentlyContinue
$wsusServerFromRegistry = $wsusServerReg.WUServer
$statisticsServerFromRegistry = $wsusServerReg.WUStatusServer
# If either server is configured, add the servers to the active settings object and check for GPOs
if ($wsusServerFromRegistry -or $statisticsServerFromRegistry) {
$checkForGPOs = $true
$ActiveWSUSSettings."WSUS Settings Source" = "Registry"
if ($wsusServerFromRegistry) {
Write-Host -Object "[Info] WSUS Update Server detected in the registry: $wsusServerFromRegistry"
} else {
$wsusServerFromRegistry = "Not Configured"
Write-Host -Object "[Warning] The WSUS Update Server is not configured. The update server is required for WSUS to function correctly."
}
if ($statisticsServerFromRegistry) {
Write-Host -Object "[Info] WSUS Statistics Server detected in the registry: $statisticsServerFromRegistry"
} else {
$statisticsServerFromRegistry = "Not Configured"
Write-Host -Object "[Warning] The WSUS Statistics Server is not configured. The statistics server is required for WSUS to function correctly."
}
} else {
# If neither server is configured, then WSUS is not configured via GPO and we can skip checking for GPOs
$checkForGPOs = $false
$wsusServerFromRegistry = "Not Configured"
$statisticsServerFromRegistry = "Not Configured"
Write-Host -Object "[Warning] No WSUS servers were detected in the registry."
}
# Add the WSUS server and statistics server to the active settings object
$ActiveWSUSSettings | Add-Member -MemberType NoteProperty -Name "Update Server" -Value $wsusServerFromRegistry
$ActiveWSUSSettings | Add-Member -MemberType NoteProperty -Name "Statistics Server" -Value $statisticsServerFromRegistry
}
else {
$checkForGPOs = $false
Write-Host -Object "`n[Info] The registry key '$wsusRegPath' was not found. WSUS is not configured on this device.`n"
}
# Check for GPO setting the WSUS configuration
if ($IsDomainJoined -and $checkForGPOs) {
Write-Host -Object "`n[Info] Checking for GPOs that configure WSUS settings..."
# Define the WSUS registry path
$wsusRegistryPath = "Software\\Policies\\Microsoft\\Windows\\WindowsUpdate"
# Get the domain name
try {
$domainName = (Get-CimInstance -Class Win32_ComputerSystem -ErrorAction Stop).Domain
} catch {
Write-Host -Object "[Error] Failed to retrieve the domain name."
Write-Host -Object "[Error] $($_.Exception.Message)"
exit 1
}
# Define the GPO paths to search for Registry.pol files
if (Test-IsServer) {
# If running on a server, the local group policy folders in System32 are not populated, so use the SYSVOL path instead
$gpoFolderPaths = @(
"\\$domainName\SYSVOL\$domainName\Policies\"
)
} else {
$gpoFolderPaths = @(
"$env:windir\System32\GroupPolicy\"
"$env:windir\System32\GroupPolicyUsers\"
)
}
# Search for WSUS settings in group policy Registry.pol files
$gposAffectingWSUS = foreach ($folderPath in $gpoFolderPaths) {
$registryPolFiles = Get-ChildItem -Path $folderPath -Filter "Registry.pol" -Recurse -File -ErrorAction SilentlyContinue -ErrorVariable registryPolFileErrors
foreach ($errorInstance in $registryPolFileErrors) {
Write-Host -Object "[Error] Error encountered while retrieving Registry.pol files from '$folderPath'."
Write-Host -Object "[Error] $($errorInstance.Exception.Message)"
$ExitCode = 1
}
# For each Registry.pol file found, read its contents and check for WSUS settings
foreach ($polFile in $registryPolFiles) {
# Read the contents of the Registry.pol file, convert to string, and remove null characters
$polContent = (Get-Content -Path $polFile.FullName -ErrorAction SilentlyContinue | Out-String) -replace "`0"
# If the content of the Registry.pol file contains the WSUS registry path, extract the settings
if ($polContent -match $wsusRegistryPath) {
# Extract the GPO ID from the file path
$GPOId = $polFile.FullName -replace ".*\\Policies\\(.*)\\Machine\\Registry.pol", '$1'
# Continue to the next file if the GPO ID is not in the expected format
if ($GPOId -notmatch "{\w{8}-\w{4}-\w{4}-\w{4}-\w{12}}") {
Write-Host -Object "[Error] Invalid GPO ID format found in $($polFile.FullName): $GPOId"
$ExitCode = 1
continue
}
# Define a regex pattern for capturing the WSUS settings from the Registry.pol file
$regexPattern = "(?<disable>\*\*del\.)?WUServer;(.+?;){2}(?<UpdateServer>.+?)].+?WUStatusServer;(.+?;){2}(?<StatisticsServer>.+?)]"
# Use regex to match the WSUS settings
$regexMatches = [regex]::Match($polContent, $regexPattern)
# Check if GPO is disabling or enabling WSUS
if ($regexMatches.Groups['disable'].Value) {
$GPOStatus = "Disabled"
} else {
$GPOStatus = "Enabled"
}
# Extract the servers
$WUServer = $regexMatches.Groups['UpdateServer'].Value
$WUStatisticsServer = $regexMatches.Groups['StatisticsServer'].Value
# Create a custom object with the GPO ID and WSUS settings
[PSCustomObject]@{
Id = $GPOId
GPOStatus = $GPOStatus
UpdateServer = $WUServer
StatisticsServer = $WUStatisticsServer
}
}
}
}
# If GPOs were found that affect WSUS settings, filter them to only include those that are active
if ($gposAffectingWSUS) {
# Define the registry path to currently applied GPOs that use Administrative Templates
$gpoHistoryPath = "HKLM:\Software\Microsoft\Windows\CurrentVersion\Group Policy\History\{35378EAC-683F-11D2-A89A-00C04FBBCFA2}"
# Retrieve active GPOs on this device from the registry path
try {
$activeGPOs = Get-ChildItem -Path $gpoHistoryPath -ErrorAction Stop | ForEach-Object { Get-ItemProperty -Path $_.PSPath -Name DisplayName, GPOName -ErrorAction Stop }
} catch {
Write-Host -Object "[Error] Failed to retrieve active GPOs from the registry."
Write-Host -Object "[Error] $($_.Exception.Message)"
exit 1
}
# Filter the GPOs that affect WSUS settings to only include those that are active
$gposAffectingWSUS = $gposAffectingWSUS | Where-Object { $_.Id -in $activeGPOs.GPOName }
}
# If any active GPOs are found that affect WSUS settings, find their display names and add them to the objects
if ($gposAffectingWSUS) {
Write-Host -Object "[Info] Found GPOs that affect WSUS settings."
$gposAffectingWSUS = $gposAffectingWSUS | ForEach-Object {
$gpoId = $_.Id
$gpoDisplayName = $activeGPOs | Where-Object { $_.GPOName -eq $gpoId } | Select-Object -ExpandProperty DisplayName
[PSCustomObject]@{
"WSUS Settings Source" = "GPO"
"WSUS Status" = $_.GPOStatus
"GPO Display Name" = $gpoDisplayName
"Update Server" = $_.UpdateServer
"Statistics Server" = $_.StatisticsServer
}
}
# Find the active WSUS settings based on the registry and GPOs
$ActiveWSUSSettings = $gposAffectingWSUS | Where-Object { $_."WSUS Status" -eq $WSUSStatus }
# If the registry settings are configured, filter the active settings to only include those that match the registry settings
if ($wsusServerFromRegistry -ne "Not Configured") {
$ActiveWSUSSettings = $ActiveWSUSSettings | Where-Object { $_."Update Server" -eq $wsusServerFromRegistry }
}
if ($statisticsServerFromRegistry -ne "Not Configured") {
$ActiveWSUSSettings = $ActiveWSUSSettings | Where-Object { $_."Update Server" -eq $statisticsServerFromRegistry }
}
} else {
Write-Host -Object "[Info] No GPOs that affect WSUS settings were found."
}
# Validate the WSUS servers in the GPOs against the registry settings
$gposAffectingWSUS | ForEach-Object {
$UpdateServer = $_."Update Server"
$StatisticsServer = $_."Statistics Server"
# If both servers are empty, skip the validation
if ([string]::IsNullOrWhiteSpace($UpdateServer) -and [string]::IsNullOrWhiteSpace($StatisticsServer)) {
return
}
$displayName = $_."GPO Display Name"
if (-not [string]::IsNullOrWhiteSpace($UpdateServer) -and $UpdateServer -ne $wsusServerFromRegistry) {
Write-Host -Object "[Warning] The WSUS update server in the GPO '$DisplayName' ($UpdateServer) does not match the server in the registry ($wsusServerFromRegistry)."
}
if (-not [string]::IsNullOrWhiteSpace($StatisticsServer) -and $StatisticsServer -ne $statisticsServerFromRegistry) {
Write-Host -Object "[Warning] The WSUS statistics server in the GPO '$DisplayName' ($StatisticsServer) does not match the server in the registry ($statisticsServerFromRegistry)."
}
}
}
# Output all GPOs to the host if there are more than one
if ($gposAffectingWSUS.Count -gt 1) {
Write-Host "`n### All GPOs affecting WSUS settings: ###`n"
($gposAffectingWSUS | Format-List | Out-String).Trim() | Out-Host
}
# Write output object to the activity feed
if (-not [string]::IsNullOrWhiteSpace($ActiveWSUSSettings)) {
Write-Host "`n### Active WSUS settings: ###`n"
($ActiveWSUSSettings | Format-List | Out-String).Trim() | Out-Host
}
# If a custom field was specified, write to it
if ($CustomFieldName) {
# Initiate the custom field value
$customFieldValue = [System.Text.StringBuilder]::new()
# If active settings are present, format the custom field value based on the WSUS settings
if (-not [string]::IsNullOrWhiteSpace($ActiveWSUSSettings)) {
# Add the status to the custom field value
$customFieldValue.Append("WSUS Status: $WSUSStatus")
# Add the servers to the custom field value
if ($wsusServerFromRegistry -eq $statisticsServerFromRegistry) {
[void]$customFieldValue.Append(" | Update and Statistics Server: $wsusServerFromRegistry")
} else {
[void]$customFieldValue.Append(" | Update Server: $wsusServerFromRegistry | Statistics Server: $statisticsServerFromRegistry")
}
# If there are active WSUS settings from GPOs, add them to the custom field value
if ($ActiveWSUSSettings."WSUS Settings Source" -eq "GPO") {
$gpoName = $ActiveWSUSSettings."GPO Display Name"
[void]$customFieldValue.Append(" | GPO Name: $gpoName")
}
} else {
[void]$customFieldValue.Append("WSUS Status: Not Configured")
}
# Set the custom field
try {
$customFieldValue = $customFieldValue.ToString()
Write-Host -Object "[Info] Setting the custom field '$CustomFieldName' with the value:`n$customFieldValue"
Set-CustomField -Name $CustomFieldName -Value $customFieldValue -Type "Text" -ErrorAction Stop
Write-Host -Object "[Info] Successfully set the custom field '$CustomFieldName'."
} catch {
Write-Host -Object "[Error] Error setting the custom field '$CustomFieldName'"
Write-Host -Object "[Error] $($_.Exception.Message)"
$ExitCode = 1
}
}
exit $ExitCode
}
end {
}
Detailed Breakdown
At a high level, the script performs the following functions:
Validation and Privilege Checks
- Ensures the script is run with Administrator privileges.
- Confirms the device is domain-joined to support GPO checks.
- Validates the custom field name if one is supplied.
Group Policy Refresh
- If domain-joined, invokes gpupdate.exe /force to refresh local policy.
Registry Inspection
- Check the HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate key.
- Retrieves WUServer and WUStatusServer values.
- Determines the state of UseWUServer to conclude if WSUS is enabled, disabled, or not configured.
GPO Analysis
- Examines local and SYSVOL-stored Registry.pol files for WSUS-related configurations.
- Extracts GPO IDs and matches them against active GPOs listed in the Windows Registry.
- Validates GPO configurations against registry entries and flags discrepancies.
Reporting and Optional Output
- Formats findings into a structured object.
- Optionally writes results to a NinjaOne text custom field, if -CustomFieldName is provided.
Potential Use Cases
Imagine an MSP overseeing patch compliance for 300 endpoints across multiple domains. During an audit, discrepancies emerge between reported WSUS configurations and actual update behavior. By deploying this script via NinjaOne, the MSP can instantly verify:
- Which machines have WSUS enabled.
- If the configuration source is a GPO or manual registry edit.
- Whether there’s a mismatch in expected WSUS URLs.
The custom field output can be compiled into a dashboard or report to identify non-compliant systems and prioritize remediation.
Comparisons
Other methods to audit WSUS settings include:
- Manual Registry Inspection via regedit or reg query.
- GPMC (Group Policy Management Console) for reviewing applied GPOs.
- RSOP (Resultant Set of Policy) or gpresult for policy tracing.
These approaches are time-consuming, lack automation, and do not scale well across endpoints. By contrast, this PowerShell script is:
- Fully automatable via NinjaOne.
- Capable of combining registry and GPO insights.
- Designed to work in both standalone and domain-joined environments.
Implications
Misconfigured WSUS settings can have serious implications:
- Security Risks: Outdated machines are more vulnerable to exploits.
- Compliance Violations: Failing audits due to inconsistent patching.
- Operational Overhead: Manual checks consume valuable IT time.
By surfacing both the source and status of WSUS settings, the script mitigates these risks and fosters proactive infrastructure management.
Recommendations
- Run periodically via RMM automation to ensure continued compliance.
- Set the custom field name explicitly to maintain uniform reporting.
- Validate GPO conflicts using the warnings generated by the script.
- Include in your onboarding script sets for new machines.
Final Thoughts
WSUS plays a pivotal role in any patch management strategy, but ensuring it’s configured correctly requires deeper visibility than what native tools often offer. This PowerShell script provides a comprehensive, scalable solution to audit WSUS configurations across your fleet.
For MSPs and IT administrators using NinjaOne, integrating this script into your automation toolkit adds immediate value. It transforms an otherwise tedious manual check into a streamlined, reportable action—strengthening both compliance and confidence in your infrastructure management practices.