Out-of-band patches—those issued outside of Microsoft’s regular Patch Tuesday cycle—often address critical vulnerabilities or stability issues that demand immediate action. In the fast-paced environments of managed service providers (MSPs) and IT operations, automating the deployment of these patches can drastically reduce response times and improve security posture. A robust PowerShell script that handles both local and remote patch installations provides a reliable and repeatable method to manage these situations.
This article breaks down a PowerShell script designed to install out-of-band patches using a given URL or file path. We’ll explore how it works, why it’s useful, and how it stacks up against other update strategies.
Background
Microsoft occasionally releases out-of-band updates to mitigate urgent security threats or resolve major stability issues. These updates are typically delivered in .msu
(Microsoft Update Standalone) files. Admins must act quickly, especially when these patches close zero-day vulnerabilities or major bugs. However, manually downloading and installing patches across multiple systems is inefficient and error-prone.
This PowerShell script fills that gap. It enables IT professionals to automate the installation process using either a direct URL to the update or a local file path. It’s particularly valuable in managed environments where administrative consistency, traceability, and speed are essential.
The Script
#Requires -Version 5.1 <# .SYNOPSIS Installs an out-of-band patch given a URL or a local path. .DESCRIPTION Installs an out-of-band patch given a URL or a local path. 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). .EXAMPLE -MSU 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2024/07/windows10.0-kb5040427-x64_750f2819b527034dcdd10be981fa82d140767f8f.msu' URL 'https://catalog.s.download.windowsupdate.com/d/msdownload/update/software/secu/2024/07/windows10.0-kb5040427-x64_750f2819b527034dcdd10be981fa82d140767f8f.msu' was given. Downloading the file... Waiting for 14 seconds. Download Attempt 1 Installing update at C:\Windows\TEMP\windowsupdate-1135150612.msu. Exit Code: 3010 [Warn] A reboot is required for this update to take effect. PRESET PARAMETER: -MSU "https://www.replace.me" Specify either a link or a file path to the patch you would like to install. PRESET PARAMETER: -ForceReboot Reboot the computer after successfully installing the requested patch. .NOTES Minimum OS Architecture Supported: Windows 10, Windows Server 2016 Release Notes: Initial Release #> [CmdletBinding()] param ( [Parameter()] [String]$MSU, [Parameter()] [Switch]$ForceReboot = [System.Convert]::ToBoolean($env:forceReboot) ) begin { if ($env:urlOrLocalPathToMsu -and $env:urlOrLocalPathToMsu -notlike "null") { $MSU = $env:urlOrLocalPathToMsu } # Check if $MSU is not provided if (!$MSU) { Write-Host -Object "[Error] An MSU was not provided. Please provide either a local file path or a URL to download the MSU." exit 1 } # Remove quotations if given quotations if ($MSU) { if ($MSU.Trim() -match "^'" -or $MSU.Trim() -match "'$" -or $MSU.Trim() -match '^"' -or $MSU.Trim() -match '"$') { $QuotationsFound = $true } $MSU = ($MSU.Trim() -replace "^'" -replace "'$" -replace '^"' -replace '"$').Trim() if ($QuotationsFound) { Write-Host -Object "[Warning] Removing quotations from your path. Your new path is '$MSU'." } } # Check if $MSU is not a local file path if ($MSU -notmatch '^[A-Za-z]:\\') { # Check if $MSU starts with 'http' but not 'http[s]?://' if ($MSU -match '^http' -and $MSU -notmatch '^http[s]?://') { Write-Host -Object "[Error] URL '$MSU' is malformed." exit 1 } # Check if $MSU contains double 'http[s]?://' if ($MSU -match '^http[s]?://http[s]?://') { Write-Host -Object "[Error] URL '$MSU' is malformed." exit 1 } # Add 'https://' to $MSU if it does not start with 'http' if ($MSU -notmatch '^http') { $MSU = "https://$MSU" Write-Host -Object "[Warn] Missing http(s) from URL, changing URL to '$MSU'." } # Check if $MSU has a top-level domain if ($MSU -notmatch '.*\..*') { Write-Host -Object "[Error] No top-level domain found in URL." exit 1 } # Validate $MSU as a URI try { [System.Uri]$MSU | Out-Null } catch { Write-Host -Object "[Error] URL '$MSU' is malformed." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } } # Check if $MSU is a local file path if ($MSU -match '^[A-Za-z]:\\') { # Check if $MSU contains invalid characters if ($MSU -match '[<>"/\|?]' -or $MSU -match ':.*:' -or $MSU -match '::') { Write-Host -Object "[Error] The file path '$MSU' contains one of the following invalid characters: < > : `" / \ | ? :" exit 1 } # Check if the file at $MSU exists if (!(Test-Path -Path "$MSU" -ErrorAction SilentlyContinue)) { Write-Host -Object "[Error] File does not exist at path '$MSU'." exit 1 } # Try to get the item at $MSU path try { $MSUFile = Get-Item -Path $MSU -Force -ErrorAction Stop } catch { Write-Host -Object "[Error] Failed to retrieve file at path '$MSU'." Write-Host -Object "[Error] $($_.Exception.Message)" exit 1 } # Check if more than one file is found at $MSU path if ($MSUFile.Count -gt 1) { Write-Host -Object "[Error] Too many files were found at path '$MSU'; please be more specific." } # Check if the $MSU path is a folder if ($MSUFile.PSIsContainer) { Write-Host -Object "[Error] The given path '$MSU' is a folder and not an MSU file." exit 1 } } # Utility function for downloading files. function Invoke-Download { param( [Parameter()] [String]$URL, [Parameter()] [String]$Path, [Parameter()] [int]$Attempts = 3, [Parameter()] [Switch]$SkipSleep ) Write-Host -Object "URL '$URL' was given." Write-Host -Object "Downloading the file..." $SupportedTLSversions = [enum]::GetValues('Net.SecurityProtocolType') if ( ($SupportedTLSversions -contains 'Tls13') -and ($SupportedTLSversions -contains 'Tls12') ) { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol::Tls13 -bor [System.Net.SecurityProtocolType]::Tls12 } elseif ( $SupportedTLSversions -contains 'Tls12' ) { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12 } else { Write-Warning -Message "TLS 1.2 and or TLS 1.3 are not supported on this system. This download may fail!" if ($PSVersionTable.PSVersion.Major -lt 3) { Write-Warning -Message "PowerShell 2 / .NET 2.0 doesn't support TLS 1.2." } } $i = 1 While ($i -le $Attempts) { # Some cloud services have rate-limiting if (-not ($SkipSleep)) { $SleepTime = Get-Random -Minimum 3 -Maximum 15 Write-Host -Object "Waiting for $SleepTime seconds." Start-Sleep -Seconds $SleepTime } if ($i -ne 1) { Write-Host "" } Write-Host -Object "Download Attempt $i" $PreviousProgressPreference = $ProgressPreference $ProgressPreference = 'SilentlyContinue' try { # Invoke-WebRequest is preferred because it supports links that redirect, e.g., https://t.ly if ($PSVersionTable.PSVersion.Major -lt 4) { # Downloads the file $WebClient = New-Object System.Net.WebClient $WebClient.DownloadFile($URL, $Path) } else { # Standard options $WebRequestArgs = @{ Uri = $URL OutFile = $Path MaximumRedirection = 10 UseBasicParsing = $true } # Downloads the file Invoke-WebRequest @WebRequestArgs } $File = Test-Path -Path $Path -ErrorAction SilentlyContinue } catch { Write-Warning -Message "An error has occurred while downloading!" Write-Warning -Message $_.Exception.Message if (Test-Path -Path $Path -ErrorAction SilentlyContinue) { Remove-Item $Path -Force -Confirm:$false -ErrorAction SilentlyContinue } $File = $False } $ProgressPreference = $PreviousProgressPreference if ($File) { $i = $Attempts } else { Write-Warning -Message "File failed to download." Write-Host -Object "" } $i++ } if (-not (Test-Path $Path)) { Write-Host -Object "[Error] Failed to download file." Write-Host -Object "Please verify the URL of '$URL'." exit 1 } else { return $Path } } function Test-IsElevated { $id = [System.Security.Principal.WindowsIdentity]::GetCurrent() $p = New-Object System.Security.Principal.WindowsPrincipal($id) $p.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator) } if (!$ExitCode) { $ExitCode = 0 } } process { # Check if the script is running with elevated (Administrator) privileges if (!(Test-IsElevated)) { Write-Host -Object "[Error] Access Denied. Please run with Administrator privileges." exit 1 } # Check if $MSU is not a local file path and download it if it's a URL if ($MSU -notmatch '^[A-Za-z]:\\') { $MSU = Invoke-Download -URL $MSU -Path "$env:TEMP\windowsupdate-$(Get-Random).msu" $DownloadedUpdate = $True } # Set the log file location $LogLocation = "$env:TEMP\update-log-$(Get-Random).evt" # Inform the user that the update is being installed Write-Host -Object "Installing update at $MSU." # Prepare the arguments for the update process $UpdateArguments = "`"$MSU`"", "/quiet", "/norestart", "/log:`"$LogLocation`"" $UpdateProcess = Start-Process -FilePath "$env:SystemRoot\System32\wusa.exe" -ArgumentList $UpdateArguments -Wait -NoNewWindow -PassThru # Check if the log file exists and try to read it if (Test-Path -Path $LogLocation -ErrorAction SilentlyContinue) { $LogFile = Get-WinEvent -Path $LogLocation -Oldest -ErrorAction SilentlyContinue | Select-Object TimeCreated, Message -ErrorAction SilentlyContinue | ForEach-Object { "$($_.TimeCreated) $($_.Message)" } Remove-Item -Path $LogLocation -Force -ErrorAction SilentlyContinue } # Remove the downloaded update file if it was downloaded if ($DownloadedUpdate -and (Test-Path -Path $MSU -ErrorAction SilentlyContinue)) { Remove-Item -Path $MSU -Force } # Output the exit code of the update process Write-Host -Object "Exit Code: $($UpdateProcess.ExitCode)" # Check if the exit code indicates success $ValidExitCodes = "0", "3010" if ($ValidExitCodes -notcontains $UpdateProcess.ExitCode) { Write-Host -Object "[Error] Exit code does not indicate success!" # Output the update log if available if ($LogFile) { Write-Host -Object "`n### Update Log ###" Write-Host -Object $LogFile } exit 1 } # Inform the user if a reboot is required if (!$ForceReboot -and $UpdateProcess.ExitCode -eq 3010) { Write-Host -Object "[Warn] A reboot is required for this update to take effect." } # Schedule a reboot if requested and the update was successful if ($ForceReboot -and $ExitCode -eq 0) { Write-Host "`nScheduling reboot for $((Get-Date).AddMinutes(1)) as requested." Start-Process shutdown.exe -ArgumentList "/r /t 60" -Wait -NoNewWindow } exit $ExitCode } end { }
Detailed Breakdown
The script consists of three primary phases: validation and preprocessing, download and installation, and post-installation handling.
1. Parameter Handling and Validation
The script accepts two parameters:
-MSU
: The URL or local path to the .msu file.-ForceReboot
: A flag that, if set, triggers a system reboot post-installation.
It can also ingest these parameters from environment variables, offering flexibility for integration with deployment systems.
The script sanitizes inputs to strip unnecessary quotations and verifies the structure of the URL or file path. It checks for invalid characters and ensures that any referenced local file exists and is not a directory.
2. Download Logic
If a URL is provided, the script uses a function called Invoke-Download
to retrieve the patch file, with built-in retry logic and support for TLS 1.2 and 1.3 protocols. This function randomizes a delay before each attempt to avoid rate-limiting by content delivery networks.
3. Installation and Logging
Using wusa.exe
, Windows’ built-in update installer, the script initiates the patch process with /quiet
and /norestart
options, along with a custom log file.
Post-installation, the script evaluates the exit code:
- 0: Success
- 3010: Success, but requires reboot
If -ForceReboot
is specified and the patch was successful, the system schedules a reboot in 60 seconds using shutdown.exe
.
Potential Use Cases
Scenario: Emergency Patch Deployment
An MSP managing hundreds of endpoints receives notice of a zero-day vulnerability in Windows. Microsoft has released an out-of-band patch. Using this script, the MSP schedules a NinjaOne automation to invoke the script with the patch URL, ensuring rapid and uniform deployment across all affected endpoints.
Within hours, the patch is installed across the fleet, and reboot policies are enforced where necessary. No manual intervention is required beyond setting the initial automation.
Comparisons
Compared to GUI-based manual installations or using WSUS for emergency patching, this script offers:
Method | Pros | Cons |
Manual Install | Simple for single systems | Time-consuming, inconsistent |
WSUS / SCCM | Centralized control | Requires infrastructure setup |
PowerShell Script (this) | Fast, automatable, works remotely | Requires admin rights and testing |
This script also outperforms basic PowerShell installations that don’t handle malformed URLs, lack retry logic, or omit post-installation checks.
FAQs
Q: Can this script install cumulative updates?
A: Yes, any valid .msu
file—cumulative or individual hotfix—can be installed.
Q: What if the system isn’t running as administrator?
A: The script checks for elevation and halts execution if the script isn’t run with admin rights.
Q: How is reboot behavior managed?
A: By default, no reboot is triggered. Use the -ForceReboot
switch to initiate a reboot if the patch succeeds.
Q: Can this be used in scheduled tasks or automation tools?
A: Absolutely. It accepts environment variables and integrates seamlessly with RMM platforms like NinjaOne.
Implications
Improperly applied or delayed out-of-band patches leave systems vulnerable to exploits. Automating patch deployment ensures compliance with security policies and audit requirements. However, the forced reboot option should be used judiciously, especially in multi-user environments, to avoid disrupting productivity.
By incorporating intelligent error handling, download validation, and logging, this script minimizes risk while maximizing efficiency.
Recommendations
- Test in a staging environment before deploying to production systems.
- Use trusted URLs and validate all sources to avoid introducing malicious content.
- Enable verbose logging in production to maintain traceability.
- Combine with NinjaOne automation for mass deployment and scheduling.
- Set proper maintenance windows when using
-ForceReboot
.
Final Thoughts
Out-of-band patch management is a critical function in today’s cybersecurity landscape. This PowerShell script offers a clean, efficient, and repeatable method to install updates using either a file path or a direct URL. It includes thoughtful features like TLS detection, randomized retry attempts, and robust validation—all of which contribute to a reliable update process.
When integrated with a platform like NinjaOne, this script becomes even more powerful. NinjaOne can deploy the script across endpoints, manage environment variables, and handle post-installation reboot logic—enabling IT teams to respond rapidly to emerging threats with confidence and control.