Wes Ellis./ a personal notebook
Technology. Stories. Side projects.
A few things worth writing down.
← Back to Script Library

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.

AT A GLANCETest-WinRMReadiness.ps1
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

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.

Test-WinRMReadiness.ps1Download
<#
.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

ParameterTypeDefaultWhat it's for
-Repairswitch—Start the service and run Set-WSManQuickConfig if anything is missing. Honors -WhatIf.
-SkipNetworkProfileCheckswitch—Let the repair run when an adapter is on the Public network profile. Quick config refuses otherwise.
-ComplianceOutputswitch—Return just 'Compliant' or 'NonCompliant', for a ConfigMgr configuration item.

Run it

What's the state of WinRM on this box?

.\Test-WinRMReadiness.ps1

See what a repair would change, without changing it.

.\Test-WinRMReadiness.ps1 -Repair -WhatIf

Fix it, on a laptop that's sitting on a Public network on purpose.

.\Test-WinRMReadiness.ps1 -Repair -SkipNetworkProfileCheck

As a ConfigMgr program that only fixes machines that need it.

powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Test-WinRMReadiness.ps1 -Repair

What you'll see

Example outputvalues are illustrative
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

  1. 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.
  2. Listener. It reads WSMan:\localhost\Listener. No listener means the service is up but nothing's actually listening.
  3. Round trip. Test-WSMan -ComputerName localhost proves the whole local stack answers. It's the closest thing to what a remote caller will see.
  4. Firewall and network profile. It looks for an enabled WINRM-HTTP-In-TCP rule and flags any adapter on the Public profile, since that's the most common reason quick config refuses to run.
  5. Repair, only if asked. With -Repair, it re-enables a disabled service, starts it, and runs Set-WSManQuickConfig, the PowerShell equivalent of winrm 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 5985 from 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.