Windows Server Update Services (WSUS) joue un rôle essentiel dans la gestion centralisée des correctifs pour les environnements Windows. Pour les professionnels de l’informatique et les fournisseurs de services gérés (MSP), il est essentiel de savoir si WSUS est activé (et si oui, comment il est configuré) pour maintenir la conformité du système, la cohérence des correctifs et l’efficacité opérationnelle. Ce script PowerShell est un outil complet d’audit des paramètres WSUS, particulièrement utile dans les environnements où les paramètres peuvent être contrôlés via des objets de stratégie de groupe (GPO).
Contexte
WSUS permet aux administrateurs de gérer la distribution des mises à jour publiées par Microsoft Update. Cependant, la visibilité de la configuration peut être opaque, en particulier dans les environnements où les stratégies de groupe sont réparties entre plusieurs unités organisationnelles. L’inspection manuelle des clés de registre et des chemins d’accès aux stratégies de groupe (GPO) est à la fois inefficace et source d’erreurs.
Ce script PowerShell a été conçu pour répondre à cette complexité. Il vérifie non seulement si WSUS est activé, mais détermine également si la configuration provient de valeurs de registre ou de GPO. De plus, il offre la possibilité d’enregistrer les résultats dans un champ personnalisé NinjaOne, ce qui permet une visibilité au niveau de l’actif dans les outils de surveillance et de gestion à distance (RMM).
Le 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 {
}
Description détaillée
À un niveau élevé, le script remplit les fonctions suivantes :
Validation et contrôle des privilèges
- Assure que le script est exécuté avec des privilèges d’administrateur.
- Confirme que l’appareil est relié à un domaine pour prendre en charge les vérifications des GPO.
- Valide le nom du champ personnalisé s’il est fourni.
Actualisation de la stratégie de groupe
- Si le domaine est joint, invoque gpupdate.exe /force pour actualiser la stratégie locale.
Inspection du registre
- Vérifier la clé HKLM:\Software\Policies\Microsoft\Windows\WindowsUpdate.
- Récupère les valeurs de WUServeret WUStatusServer.
- Détermine l’état de UseWUServer pour conclure si WSUS est activé, désactivé ou non configuré.
Analyse de GPO
- Examine les fichiers Registry.pol locaux et stockés dans SYSVOL pour y trouver des configurations liées à WSUS.
- Extrait les identifiants de GPO et les compare aux GPO répertoriés dans le registre Windows.
- Valide les configurations GPO par rapport aux entrées de registre et signale les divergences.
Rapports et sorties optionnelles
- Formate les résultats dans un objet structuré.
- Écrit éventuellement les résultats dans un champ personnalisé de texte NinjaOne, si -CustomFieldName est fourni.
Cas d’utilisation potentiels
Imaginez une entreprise MSP qui supervise la conformité des correctifs pour 300 terminaux dans plusieurs domaines. Lors d’un audit, des divergences apparaissent entre les configurations WSUS rapportées et le comportement réel des mises à jour. En déployant ce script via NinjaOne, l’entreprise MSP peut instantanément vérifier :
- Quelles sont les machines pour lesquelles WSUS est activé.
- Si la source de configuration est un GPO ou une modification manuelle du registre.
- S’il y a une incohérence dans les URL WSUS attendues.
Les résultats des champs personnalisés peuvent être compilés dans un tableau de bord ou un rapport afin d’identifier les systèmes non conformes et d’établir des priorités en matière de remédiation.
Comparaisons
D’autres méthodes permettent d’auditer les paramètres WSUS :
- Inspection manuelle du registre via regedit ou reg query.
- GPMC (Group Policy Management Console) pour la révision des GPO appliqués.
- RSOP (Resultant Set of Policy) ou gpresult pour le suivi des stratégies.
Ces approches prennent du temps, manquent d’automatisation et ne s’adaptent pas bien à l’ensemble des terminaux. En revanche, ce script PowerShell est :
- Entièrement automatisable via NinjaOne.
- Capable de combiner des informations sur le registre et les GPO.
- Conçu pour fonctionner dans des environnements autonomes ou reliés à un domaine.
Questions fréquentes
Dois-je exécuter ce script en tant qu’administrateur ?
Oui, il vérifie l’élévation et s’arrête s’il n’est pas exécuté avec les privilèges appropriés.
Que faire si mes paramètres WSUS ne sont pas appliqués via une stratégie de groupe ?
Le script détectera toujours les configurations basées sur le registre et les signalera en conséquence.
Puis-je l’utiliser sans NinjaOne ?
Oui, bien que l’écriture de champs personnalisés soit optionnelle et spécifique à NinjaOne, les fonctions de détection fonctionnent indépendamment.
Cela fonctionnera-t-il sur Windows 10 et Windows Server 2016+ ?
Oui. Le script cible ces systèmes et les systèmes supérieurs.
Implications
Une mauvaise configuration de WSUS peut avoir de graves conséquences :
- Risques pour la sécurité : Les machines obsolètes sont plus vulnérables aux exploits.
- Violations de la conformité : Échec des audits en raison d’une application irrégulière des correctifs.
- Frais généraux d’exploitation : Les vérifications manuelles consomment un temps précieux pour les équipes informatiques.
En faisant apparaître la source et l’état des paramètres WSUS, le script atténue ces risques et favorise une gestion proactive de l’infrastructure.
Recommandations
- Exécutez périodiquement via l’automatisation RMM pour garantir une conformité continue.
- Définissez le nom du champ personnalisé explicitement pour maintenir un rapport uniforme.
- Validez les conflits de GPO en utilisant les avertissements générés par le script.
- Incluez dans votre script dans l’onboarding pour les nouvelles machines.
Conclusion
WSUS joue un rôle central dans toute stratégie de gestion des correctifs, mais s’assurer qu’il est configuré correctement nécessite une visibilité plus approfondie que celle qu’offrent souvent les outils natifs. Ce script PowerShell fournit une solution complète et évolutive pour auditer les configurations WSUS sur l’ensemble de votre parc informatique.
Pour les MSP et les administrateurs informatiques qui utilisent NinjaOne, l’intégration de ce script dans leur boîte à outils d’automatisation apporte une valeur ajoutée immédiate. Il transforme un contrôle manuel fastidieux en une action optimisée, pouvant faire l’objet d’un rapport, ce qui renforce à la fois la conformité et la confiance dans vos pratiques de gestion de l’infrastructure.