Guide to Diagnosing Windows Update Issues Using PowerShell

Diagnosing Windows Update issues can be a tedious task for IT professionals, especially when managing multiple machines. Delays or errors in the update process can lead to security vulnerabilities, compliance issues, and general system instability.

The provided PowerShell script offers a streamlined approach to diagnosing and addressing common Windows Update problems, ensuring that systems remain up-to-date and secure. This blog post delves into the script’s functionality, exploring how it can be an invaluable tool for IT professionals and Managed Service Providers (MSPs) alike.

Background

Windows Update is a critical component in maintaining the health and security of a Windows-based system. However, various factors can hinder its smooth operation, from service misconfigurations to network issues. IT professionals often face the daunting task of troubleshooting these issues manually, which can be time-consuming and prone to human error.

The PowerShell script provided addresses this challenge by automating the diagnostic process, ensuring that common issues are identified and resolved quickly. By using this script, IT teams can maintain system integrity, reduce downtime, and improve overall efficiency.

The Script:

#Requires -Version 5.1

<#
.SYNOPSIS
    Diagnose Windows Update issues.
.DESCRIPTION
    Checks that CryptSvc, and bits or running or not
    Checks that wuauserv is running and the startup type is set correctly.
    Checks WaaSMedic plugins doesn't have issues. (Only applies to OS Build Version is greater than 17600).
    Checks if NTP is setup.
    Checks Windows Update logs for any errors in the last week.

.EXAMPLE
    (No Parameters)
    ## EXAMPLE OUTPUT WITHOUT PARAMS ##
    [Info] Last checked for updates on 4/29/2023
    [Issue] Windows Update has not checked for updates in over 30 days.

PARAMETER: -ResultsCustomField WindowsUpdate
    Saves results to a multi-line custom field.
.EXAMPLE
    -ResultsCustomField WindowsUpdate
    ## EXAMPLE OUTPUT WITH ResultsCustomField ##
    [Info] Last checked for updates on 4/29/2023
    [Issue] Windows Update has not checked for updates in over 90 days.
.OUTPUTS
    None
.NOTES
    Minimum OS Architecture Supported: Windows 10, Windows Server 2016
    Release Notes: Initial Release
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).
#>

[CmdletBinding()]
param (
    [int]$Days = 30,
    [string]$ResultsCustomField
)

begin {
    if ($env:Days) {
        $Days = $env:Days
    }
    if ($env:resultscustomfield -notlike "null") {
        $ResultsCustomField = $env:resultscustomfield
    }
    function Test-IsElevated {
        $id = [System.Security.Principal.WindowsIdentity]::GetCurrent()
        $p = New-Object System.Security.Principal.WindowsPrincipal($id)
        $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
    }
    function Test-WaaSMedic {
        [CmdletBinding()]
        param()
        $WaaS = 0
        Try {
            $WaaS = New-Object -ComObject "Microsoft.WaaSMedic.1"
        }
        Catch {
            Write-Host "WaaS Medic Support: No"
        }
    
        Try {
            if ($WaaS -ne 0) {
                Write-Host "WaaS Medic Support: Yes"
                $Plugins = $WaaS.LaunchDetectionOnly("Troubleshooter")
    
                if ($Plugins -eq "") {
                    [PSCustomObject]@{
                        Id        = "WaaSMedic"
                        Detected  = $false
                        Parameter = @{"error" = $Plugins }
                    }
                }
                else {
                    [PSCustomObject]@{
                        Id        = "WaaSMedic"
                        Detected  = $true
                        Parameter = @{"error" = $Plugins }
                    }
                    "Plugins that might have errors: " + $Plugins | Out-String | Write-Host
                }
            }
        }
        Catch {
            Write-Host "WaaS Medic Detection: Failed"
        }
        Finally {
            # Release COM Object if we aren't running test cases
            if (-not $env:NinjaPesterTesting) {
                [System.Runtime.Interopservices.Marshal]::ReleaseComObject($WaaS) | Out-Null
            }
        }
    }
    function Get-TimeSyncType {
        [string]$result = ""
        [string]$registryKey = "HKLM:\SYSTEM\CurrentControlSet\Services\W32Time\Parameters"
        [string]$registryKeyName = "Type"
    
        if ((Test-Path $registryKey -ErrorAction SilentlyContinue)) {
            $registryEntry = Get-Item -Path $registryKey -ErrorAction SilentlyContinue
            if ($null -ne $registryEntry) {
                return Get-ItemPropertyValue -Path $registryKey -Name $registryKeyName
            }
        }
        return $result
    }
    function Test-ConnectedToInternet {
        $NLMType = [Type]::GetTypeFromCLSID('DCB00C01-570F-4A9B-8D69-199FDBA5723B')
        $INetworkListManager = [Activator]::CreateInstance($NLMType)  
        return ($INetworkListManager.IsConnectedToInternet -eq $true)
    }
    function Get-ComponentAndErrorCode([string]$msg) {	
        $Codes = [regex]::matches($msg, "0x[a-f0-9a-f0-9A-F0-9A-F0-9]{6,8}")
        if ($Codes.count -gt 1) {
            $CodeList = ""
            # there can be more than one error code can be returned for the same component at once
            foreach ($Code in $Codes) {
                $CodeList += "_" + $Code
            }
            return $CodeList
        }
        else {
            return $Codes[0].Value
        }
    }
    function Get-DatedEvents($EventLog) {
        $DatedEvents = @()
        if ($null -eq $EventLog) {
            return $null 
        }
        foreach ($Event in $EventLog) {
            #$eventMsg = $event.Message
            $DatedEvents += $Event.Message
        }
        return $DatedEvents
    }
    function Get-SystemEvents($EventSrc, $Time) {
        $Events = Get-WinEvent -ProviderName $EventsSrc -ErrorAction 0 | Where-Object { ($_.LevelDisplayName -ne "Information") -and (($_.Id -eq 20) -or ($_.Id -eq 25)) -and ($_.TimeCreated -gt $Time) }
        return $Events
    }
    function Get-HasWinUpdateErrorInLastWeek([switch]$AllLastWeekError) {
        $Events = @()
        $EventsSrc = "Microsoft-Windows-WindowsUpdateClient"
        $startTime = (Get-Date) - (New-TimeSpan -Day 8)
        $wuEvents = Get-SystemEvents $EventsSrc $startTime
        if ($null -eq $wuEvents) {
            return $null
        }
        $Events += Get-DatedEvents $wuEvents
        $LatestError = Get-ComponentAndErrorCode $Events[0]
        $ErrorList = @{}
        $ErrorList.add("latest", $LatestError)
        if ($AllLastWeekError) {
            foreach ($str in $Events) {
                $ECode = Get-ComponentAndErrorCode $str
                if ($null -ne $ECode -and !$ErrorList.ContainsValue($ECode)) {
                    $ErrorList.add($ECode, $ECode)
                }
            }
        }
        return $ErrorList
    }
    Function Get-LocalTime($UTCTime) {
        $strCurrentTimeZone = (Get-CimInstance -ClassName Win32_TimeZone).StandardName
        # If running test cases return current date
        if ($env:NinjaPesterTesting) {
            return Get-Date
        }
        $TZ = [System.TimeZoneInfo]::FindSystemTimeZoneById($strCurrentTimeZone)
        Return [System.TimeZoneInfo]::ConvertTimeFromUtc($UTCTime, $TZ)
    }
    $IssuesFound = $false
    $Log = [System.Collections.Generic.List[String]]::new()
}
process {
    if (-not (Test-IsElevated)) {
        Write-Error -Message "Access Denied. Please run with Administrator privileges."
        exit 1
    }

    if (-not $(Test-ConnectedToInternet)) {
        Write-Host "[Issue] Windows doesn't think it is connected to Internet."
        $IssuesFound = $true
    }

    # Check CryptSvc amd bits services
    $Service = Get-Service -Name CryptSvc
    if ($Service.StartType -notlike 'Automatic') {
        Write-Host "[Issue] (CryptSvc) CryptSvc service is set to $($Service.StartType) but needs to be set to Automatic"
        $Log.Add("[Issue] (CryptSvc) CryptSvc service is set to $($Service.StartType) but needs to be set to Automatic")
        $IssuesFound = $true
    }
    else {
        Write-Host "[Info] (CryptSvc) CryptSvc service is set to $($Service.StartType)"
        $Log.Add("[Info] (CryptSvc) CryptSvc service is set to $($Service.StartType)")
    }

    $Service = Get-Service -Name bits
    if ($Service.StartType -eq 'Disabled') {
        Write-Host "[Issue] (bits) BITS service is set to $($Service.StartType) but needs to be set to Manual"
        $Log.Add("[Issue] (bits) BITS service is set to $($Service.StartType) but needs to be set to Manual")
        $IssuesFound = $true
    }
    else {
        Write-Host "[Info] (bits) BITS service is set to $($Service.StartType)"
        $Log.Add("[Info] (bits) BITS service is set to $($Service.StartType)")
    }

    # Check that Windows Update service is running and isn't disabled
    $wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
    if ($wuService.Status -ne "Running") {
        $Service = Get-Service -Name wuauserv
        if ($Service.StartType -eq 'Disabled') {
            Write-Host "[Issue] (wuauserv) Windows Update service is set to $($Service.StartType) but needs to be set to Automatic (Trigger Start) or Manual"
            $Log.Add("[Issue] (wuauserv) Windows Update service is set to $($Service.StartType) but needs to be set to Automatic (Trigger Start) or Manual")
            $IssuesFound = $true
        }
        else {
            Write-Host "[Info] (wuauserv) Windows Update service is set to $($Service.StartType)"
            $Log.Add("[Info] (wuauserv) Windows Update service is set to $($Service.StartType)")
        }
    }

    # Check WaaSMedic
    $SupportWaaSMedic = [System.Environment]::OSVersion.Version.Build -gt 17600
    if ($SupportWaaSMedic) {
        $Plugins = Test-WaaSMedic
        $PluginIssues = $Plugins | Where-Object { $_.Parameter["error"] } | ForEach-Object {
            $PluginErrors = $_.Parameter["error"]
            "[Potential Issue] WaaSMedic plugin errors found with: $($PluginErrors)"
        }
        if ($PluginIssues.Count -gt 1) {
            Write-Host "[Issue] Found more than 1 plugin errors."
            $Log.Add("[Issue] Found more than 1 plugin errors.")
            $PluginIssues | Write-Host
            $IssuesFound = $true
        }
    }

    # Check if NTP is setup
    if ("NoSync" -eq (Get-TimeSyncType)) {
        Write-Host "[Issue] NTP not setup!"
        $Log.Add("[Issue] NTP not setup!")
        $IssuesFound = $true
    }

    # Check Windows Update logs
    $EventErrors = Get-HasWinUpdateErrorInLastWeek -AllLastWeekError
    if ($EventErrors.Count -gt 0) {
        if (![string]::IsNullOrEmpty($allError.Values)) {
            Write-Host "[Issue] Event Log has Windows Update errors."
            $Log.Add("[Issue] Event Log has Windows Update errors.")
            $errorCodes = $allError.Values -join ';'
            Write-Host "[Issue] Error codes found: $errorCodes"
            $Log.Add("[Issue] Error codes found: $errorCodes")
            $IssuesFound = $true
        }
    }

    # If no issues found, get number of days since the last check for updates happened
    if (-not $IssuesFound) {
        $LastCheck = Get-LocalTime $(New-Object -ComObject Microsoft.Update.AutoUpdate).Results.LastSearchSuccessDate

        Write-Host "[Info] Last checked for updates on $($LastCheck.ToShortDateString())"
        $Log.Add("[Info] Last checked for updates on $($LastCheck.ToShortDateString())")

        $LastCheckTimeSpan = New-TimeSpan -Start $LastCheck -End $(Get-Date)
        if ($LastCheckTimeSpan.TotalDays -gt $Days) {
            $Days = [System.Math]::Round($LastCheckTimeSpan.TotalDays, 0)
            Write-Host "[Issue] Windows Update has not checked for updates in over $Days days."
            $Log.Add("[Issue] Windows Update has not checked for updates in over $Days days.")
            $IssuesFound = $true
        }
    }

    if ($ResultsCustomField) {
        Ninja-Property-Set -Name $ResultsCustomField -Value $($Log | Out-String)
    }

    if ($IssuesFound) {
        exit 1
    }
    exit 0
}
end {
    
    
    
}

 

Access over 300+ scripts in the NinjaOne Dojo

Get Access

Detailed Breakdown

The script operates by conducting a series of checks on key Windows Update components and services, each essential for the update process to function correctly. Here’s a step-by-step breakdown of how the script works:

1. Elevation Check: The script begins by verifying that it is being run with administrator privileges, which are required to modify system services and access specific logs.

2. Internet Connectivity Test: It checks whether the system is connected to the internet, a basic requirement for downloading updates.

3. Service Status Checks:

  • Cryptographic Services (CryptSvc): Ensures that the Cryptographic Services are set to ‘Automatic,’ a necessary configuration for handling update files securely.
  • Background Intelligent Transfer Service (BITS): Verifies that BITS is not disabled, as it is responsible for transferring files in the background, including updates.
  • Windows Update Service (wuauserv): Confirms that the Windows Update service is running and set to the correct startup type.

4. WaaSMedic Check: For systems with a build version greater than 17600, the script checks the WaaSMedic plugins, which are responsible for fixing update-related issues automatically.

5. NTP Configuration: The script checks if the Network Time Protocol (NTP) is configured correctly, ensuring that the system clock is synchronized with an external time source—a crucial factor for the update process.

6. Event Log Analysis: It reviews the Windows Update event logs for any errors recorded in the past week, identifying specific error codes that may indicate underlying issues.

7. Last Update Check: Finally, the script determines when the system last checked for updates. If this exceeds the user-defined threshold (default is 30 days), it flags the issue.

Each of these checks is logged, and if any issues are found, they are reported in a summary that can be saved to a custom field for further analysis.

Potential Use Cases

Imagine an IT professional managing a fleet of workstations for a large corporation. One day, multiple users report that their systems haven’t received updates in several weeks. Rather than manually checking each system, the IT professional deploys this script across all workstations.

The script identifies that the Windows Update service on several machines is misconfigured, with BITS disabled on others. It also highlights that some systems haven’t checked for updates in over 60 days.

With this information, the IT professional can quickly rectify the issues, ensuring that all systems are brought up to date, minimizing security risks, and maintaining compliance with corporate policies.

Comparisons

This PowerShell script offers a more automated and comprehensive approach compared to traditional methods, such as manually checking service statuses or sifting through event logs.

While GUI-based tools like the Windows Update Troubleshooter can address some issues, they often fall short in providing detailed insights or handling multiple machines simultaneously.

This script, on the other hand, not only identifies issues but also offers clear, actionable insights, making it a superior option for large-scale IT environments.

FAQs

1. Can this script fix the issues it finds?

  • No, this script is designed to diagnose and report issues. However, it provides enough information for IT professionals to take the necessary steps to resolve the problems manually.

2. Is this script compatible with all versions of Windows?

  • The script supports Windows 10 and Windows Server 2016 or later versions, ensuring broad applicability across modern Windows environments.

3. What should I do if the script reports an error with WaaSMedic plugins?

  • WaaSMedic issues typically require manual intervention. You may need to reset the WaaSMedic service or use additional tools to address plugin-specific errors.

Implications

The results from this script can have significant implications for IT security. Identifying and addressing Windows Update issues promptly can prevent unpatched vulnerabilities from being exploited, reducing the risk of cyberattacks.

Moreover, ensuring that updates are applied consistently helps maintain system stability, preventing unexpected downtimes that could disrupt business operations.

Recommendations

When using this script, it’s recommended to:

  • Run it regularly: Incorporate it into your routine maintenance schedule to ensure that update-related issues are caught early.
  • Analyze logs carefully: Pay attention to the details in the logs generated by the script, as they can provide critical insights into recurring issues.
  • Integrate with automation tools: For large-scale environments, consider integrating this script with automation platforms like NinjaOne to streamline the diagnostic process across multiple systems.

Final Thoughts

NinjaOne offers a powerful platform that complements the functionality of this script. By integrating the script into NinjaOne’s automated workflows, IT professionals can enhance their ability to diagnose and address Windows Update issues across numerous machines simultaneously.

This integration not only saves time but also ensures that all systems remain secure and up-to-date, ultimately contributing to a more stable and resilient IT infrastructure.

Next Steps

Building an efficient and effective IT team requires a centralized solution that acts as your core service deliver tool. NinjaOne enables IT teams to monitor, manage, secure, and support all their devices, wherever they are, without the need for complex on-premises infrastructure.

Learn more about NinjaOne Remote Script Deployment, check out a live tour, or start your free trial of the NinjaOne platform.

Categories:

You might also like

How to Monitor Log Files on macOS with a Custom Bash Script

How to Monitor Log Files and Detect Specific Text on Linux Using a Bash Script

How to Use PowerShell to Monitor Text Files and Trigger Alerts for IT Professionals

How to Automate Microsoft Safety Scanner Using a PowerShell Script

Comprehensive Guide to Using PowerShell for Efficient Event Log Searches

How to Use PowerShell to Detect Open and Established Ports in Windows

Watch Demo×
×

See NinjaOne in action!

By submitting this form, I accept NinjaOne's privacy policy.

NinjaOne Terms & Conditions

By clicking the “I Accept” button below, you indicate your acceptance of the following legal terms as well as our 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 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).