SCRIPT LIBRARY · POWERSHELL
Checking WinRM Before You Patch Remotely (Packaged for SCCM)
A read-first WinRM check that tells you why remote PowerShell won't connect, fixes it only when you ask, and doubles as a ConfigMgr compliance script.
- What it does
- Checks the WinRM service, listeners, inbound firewall rule, network profile and a local Test-WSMan round trip, then reports what's missing. With -Repair it enables WinRM properly; with -ComplianceOutput it returns a single Compliant/NonCompliant string.
- Requires
- Windows PowerShell 5.1 (PowerShell 7 works too)
- NetSecurity and NetConnection modules (built in)
- Permissions
- Local administrator to read the listener config and to repair. SYSTEM is fine.
- Runs on
- Windows 10/11, Windows Server 2016+
- Tested
- Parse-checked and dry-run with mocked service, WSMan and firewall cmdlets in PowerShell 7.4
Part 5 of the thread Windows Update, untangled
A lot of patching tooling rides on WinRM. Anything built on Invoke-Command, remote update jobs, Server Manager, the "check it from my desk" one-liners. When WinRM is off on a machine, those just fail with a wall of red text about the WS-Management service, and you get to guess which of four things is actually wrong.
Worth saying up front: Windows Update itself doesn't need WinRM, and neither does ConfigMgr's software update deployment. This is for the remote tools you run around patching.
The old version of this post was named "check" but ran winrm quickconfig -force on every machine it touched. That's a configuration change, not a check, and it opened a firewall port whether you wanted it or not. This version looks first, tells you what's missing, and only changes anything when you pass -Repair.
<#
.SYNOPSIS
Checks whether a device will accept remote PowerShell, and optionally fixes it.
.DESCRIPTION
Looks at the pieces remote management actually needs: the WinRM service, a listener,
the inbound firewall rule, and a local Test-WSMan round trip. It also flags a Public
network profile, which is the usual reason quick config refuses to run.
With -Repair it starts WinRM and runs Set-WSManQuickConfig. With -ComplianceOutput it
returns a single Compliant/NonCompliant string, which is what a ConfigMgr configuration
item discovery script wants to see.
.PARAMETER Repair
Fix what's broken. Honors -WhatIf.
.PARAMETER SkipNetworkProfileCheck
Let the repair run even when a network adapter is on the Public profile.
.PARAMETER ComplianceOutput
Return only 'Compliant' or 'NonCompliant' instead of the full object.
.EXAMPLE
.\Test-WinRMReadiness.ps1
.EXAMPLE
.\Test-WinRMReadiness.ps1 -Repair -WhatIf
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[switch]$Repair,
[switch]$SkipNetworkProfileCheck,
[switch]$ComplianceOutput
)
function Get-WinRMState {
$issues = [System.Collections.Generic.List[string]]::new()
$service = Get-Service -Name WinRM -ErrorAction SilentlyContinue
if (-not $service) { $issues.Add('WinRM service not found') }
else {
if ($service.Status -ne 'Running') { $issues.Add("WinRM service is $($service.Status)") }
if ("$($service.StartType)" -eq 'Disabled') { $issues.Add('WinRM service is disabled') }
}
# Listener and WS-Man checks only work while the service is running.
$listeners = @()
$wsman = $false
if ($service -and $service.Status -eq 'Running') {
$listeners = @(Get-ChildItem -Path WSMan:\localhost\Listener -ErrorAction SilentlyContinue)
if ($listeners.Count -eq 0) { $issues.Add('No WinRM listener configured') }
try { $null = Test-WSMan -ComputerName localhost -ErrorAction Stop; $wsman = $true }
catch { $issues.Add("Test-WSMan failed: $($_.Exception.Message)") }
}
$rules = @(Get-NetFirewallRule -Name 'WINRM-HTTP-In-TCP*' -ErrorAction SilentlyContinue | Where-Object { "$($_.Enabled)" -eq 'True' })
if ($rules.Count -eq 0) { $issues.Add('No enabled WinRM inbound firewall rule') }
$public = @(Get-NetConnectionProfile -ErrorAction SilentlyContinue | Where-Object { "$($_.NetworkCategory)" -eq 'Public' })
[pscustomobject]@{
ComputerName = $env:COMPUTERNAME
Ready = ($issues.Count -eq 0)
ServiceStatus = if ($service) { "$($service.Status)" } else { 'Missing' }
StartType = if ($service) { "$($service.StartType)" } else { $null }
Listeners = ($listeners | ForEach-Object { $_.Keys -join ' ' }) -join '; '
FirewallRules = $rules.Count
WSManResponds = $wsman
PublicNetwork = ($public.Count -gt 0)
Issues = $issues -join '; '
Action = 'None'
}
}
$state = Get-WinRMState
if ($Repair -and -not $state.Ready) {
if ($state.PublicNetwork -and -not $SkipNetworkProfileCheck) {
Write-Warning 'A network adapter is on the Public profile. Quick config will refuse to run. Use -SkipNetworkProfileCheck if that is expected.'
$state.Action = 'Skipped (Public network)'
}
elseif ($PSCmdlet.ShouldProcess($env:COMPUTERNAME, 'Enable WinRM (service, listener, firewall rule)')) {
try {
if ($state.StartType -eq 'Disabled') { Set-Service -Name WinRM -StartupType Automatic -ErrorAction Stop }
if ($state.ServiceStatus -ne 'Running') { Start-Service -Name WinRM -ErrorAction Stop }
Set-WSManQuickConfig -Force -SkipNetworkProfileCheck:$SkipNetworkProfileCheck -ErrorAction Stop
$state = Get-WinRMState
$state.Action = 'Repaired'
}
catch {
Write-Error "Repair failed: $($_.Exception.Message)"
$state.Action = 'Failed'
}
}
else { $state.Action = 'WhatIf' }
}
if ($ComplianceOutput) {
if ($state.Ready) { 'Compliant' } else { 'NonCompliant' }
}
else {
$state
}
Parameters
| Parameter | Type | Default | What it's for |
|---|---|---|---|
-Repair | switch | — | Start the service and run Set-WSManQuickConfig if anything is missing. Honors -WhatIf. |
-SkipNetworkProfileCheck | switch | — | Let the repair run when an adapter is on the Public network profile. Quick config refuses otherwise. |
-ComplianceOutput | switch | — | Return just 'Compliant' or 'NonCompliant', for a ConfigMgr configuration item. |
Run it
What's the state of WinRM on this box?
.\Test-WinRMReadiness.ps1See what a repair would change, without changing it.
.\Test-WinRMReadiness.ps1 -Repair -WhatIfFix it, on a laptop that's sitting on a Public network on purpose.
.\Test-WinRMReadiness.ps1 -Repair -SkipNetworkProfileCheckAs a ConfigMgr program that only fixes machines that need it.
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Test-WinRMReadiness.ps1 -RepairWhat you'll see
ComputerName : PC-0142
Ready : False
ServiceStatus : Stopped
StartType : Manual
Listeners :
FirewallRules : 0
WSManResponds : False
PublicNetwork : False
Issues : WinRM service is Stopped; No enabled WinRM inbound firewall rule
Action : None
How it works
- Service first. It checks that the WinRM service exists, isn't disabled and is running. Nothing else can be tested with the service stopped, so the listener and WS-Man checks are skipped until it is.
- Listener. It reads
WSMan:\localhost\Listener. No listener means the service is up but nothing's actually listening. - Round trip.
Test-WSMan -ComputerName localhostproves the whole local stack answers. It's the closest thing to what a remote caller will see. - Firewall and network profile. It looks for an enabled
WINRM-HTTP-In-TCPrule and flags any adapter on the Public profile, since that's the most common reason quick config refuses to run. - Repair, only if asked. With
-Repair, it re-enables a disabled service, starts it, and runsSet-WSManQuickConfig, the PowerShell equivalent ofwinrm quickconfig, which creates the listener and the firewall exception. Then it checks everything again and reports the new state.
In ConfigMgr, the nicest home for this is a configuration baseline. Use the -ComplianceOutput behavior as the discovery script with a rule of "equals Compliant", and the -Repair behavior as remediation. Machines fix themselves on the evaluation schedule, and the compliance report doubles as your "which boxes can I actually reach" list.
Take it further
- Check from the outside too.
Test-NetConnection PC-0142 -Port 5985from your admin box confirms that nothing between you and the machine is blocking it. - Move to HTTPS. If you manage machines outside the domain, set up an HTTPS listener with a certificate instead of loosening TrustedHosts.
- Set it by policy. Once you know which machines need it, a GPO or Intune settings catalog profile is a better long-term owner than a script.
Things that'll trip you up
- Group Policy wins. If a GPO configures "Allow remote server management through WinRM" or the WinRM firewall rules, local changes last until the next refresh. When the repair doesn't stick, check gpresult before you check anything else.
- Ready doesn't mean you can connect. This checks the receiving end. Connecting by IP address, or to a workgroup machine, falls back to NTLM and needs TrustedHosts or an HTTPS listener on the client side. Kerberos by name is the happy path.
- Third-party firewalls don't care about Windows Firewall rules. If an endpoint security product owns the firewall, the rule check can pass and port 5985 can still be closed.
- Configuration item scripts can't take parameters. ConfigMgr runs CI scripts as-is. Paste a copy with ComplianceOutput defaulted to $true for discovery, and one with Repair defaulted to $true for remediation.