Check Installed Windows Operating System Patch On Servers Using Powershell
Problem
Do you always patch your Windows Operating System every month?
We do this regularly in our servers. But this task is being done by the server administrator.
In short, for us to know what patches were installed we have to go into each server and verify them.
We should be able to do this using powershell.
The script should ask the user to provide the server name or server list file.
It also should ask the user to enter the patch information they want to search for.
Below are some of the information that I immediately wanted to know after the servers are patched:
- When did the Windows OS patching happened?
- What patches were installed?
- Were the patches successfully installed?
- Does the server require a reboot after the patching?
- If the latest patch were not installed, when was the last Windows OS patching happened and what patches were last installed?
- If the server doesn't have the latest patch, the server name needs to be stands out from the list.
Solution
I will try to explain the sections of the script that I used for each requirements.
I need the user to provide the search string information and the server name and server list.
Recently, Microsoft have started naming their Windows OS Patch with YYYY-MM prefix on the patch name/title/description.
Example title for one of the September patches:
2018-09 Update for Windows Server 2016 (1803) for x64-based Systems (KB4100347).
Now we can just provide the string 2018-09 if we want to know what September patches were installed on the server.
To get this information we need to add a parameter in our script:
PowerShell
Param(
[Parameter(Mandatory=$True)]
[string]$KBNumber_or_TitleString,
[Parameter(Mandatory=$True)]
[string]$ServerName_or_ServerList
)
I also added the Mandatory argument to force the user to provide this information.
Now lets see how we can get the other script requirements:
1. When did the Windows OS patching happened?
2. What patches were installed?
3. Were the patches successfully installed?
I was able to find the example to get all these three information from this URL:
The ResultCode to know if the patches were installed successfully is being returned as numbers.
I have to transpose them to human readable values in the script below.
PowerShell
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
if ($HistoryCount -gt 0)
{
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
$Title = $_.Title
$Result = $null
Switch ($_.ResultCode)
{
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
}
I also found out that not all the Windows Updates are stored on Microsoft.Update.Session.
Some of the Windows Patch information are installed as Hotfix.
I have to use the powershell Get-Hofix command to get the Hotfix information.
PowerShell
Get-Hotfix | Sort-Object InstalledOn,HotFixID -Descending
4. Does the server require a reboot after the patching?
When a patch needs a reboot, its ResultCode value will be 1 (In Progress) until the machine is rebooted.
This can be confirmed and verified by querying the Windows Registry Settings.
There are 3 different settings that will indicate that reboot is required but not all of them exists in the registry.
This is the reason why I used -ErrorAction Ignore in the script below.
PowerShell
$PendingRebootStatus = $null
if ($Result -eq 'InProgress')
{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { $PendingRebootStatus=$true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { $PendingRebootStatus=$true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { $PendingRebootStatus=$true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending)
{
$PendingRebootStatus = $true
}
}
catch{}
}
if ($PendingRebootStatus -eq 'True')
{
$Result = 'Pending restart'
}
5. If the latest patch were not installed, when was the last Windows OS patching happened and what patches were last installed?
6. If the server doesn't have the latest patch, the server name needs to be stands out from the list.
These 2 requirements will have the same process as requirements 1,2, and 3. But this just needs to return the last Windows OS patching information.
For the servers to be easily identifiable, I placed an * (astirisk) in front of the server name when the report gets generated.
It is now time to merge them all together in one script.
NOTE: If you are interested in making it a GUI based script you can refer to my previous posts. - Additonal information that can be added are cluster status and Operating Systems version.
PowerShell
Param(
[Parameter(Mandatory=$True)]
[string]$KBNumber_or_TitleString,
[Parameter(Mandatory=$True)]
[string]$ServerName_or_ServerList
)
clear
'Microsoft Windows Server Patch Number or Part of Patch Title String: ' + $KBNumber_or_TitleString
if (($ServerName_or_ServerList.Contains("\") -eq $True) -or ($ServerName_or_ServerList.Contains(".") -eq $True))
#-eq $True) -or (($ServerName_or_ServerList.Contains(".") -eq $True)
{
$serverlist = get-content "$ServerName_or_ServerList"
}
else
{
$serverlist = $ServerName_or_ServerList
}
$KBs = @()
foreach ($svr in $serverlist)
{
$svr = $svr.Trim()
$AllServerUpdates = $null
$AllServerUpdates = Invoke-Command -ComputerName $svr -ScriptBlock {
$KBNum = $args[0]
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
if ($HistoryCount -gt 0)
{
$Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
$Title = $null
$Title = $_.Title
$Result = $null
Switch ($_.ResultCode)
{
0 { $Result = 'NotStarted'}
1 { $Result = 'InProgress' }
2 { $Result = 'Succeeded' }
3 { $Result = 'SucceededWithErrors' }
4 { $Result = 'Failed' }
5 { $Result = 'Aborted' }
default { $Result = $_ }
}
$PendingRebootStatus = $null
if ($Result -eq 'InProgress')
{
if (Get-ChildItem "HKLM:\Software\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending" -EA Ignore) { $PendingRebootStatus=$true }
if (Get-Item "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired" -EA Ignore) { $PendingRebootStatus=$true }
if (Get-ItemProperty "HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager" -Name PendingFileRenameOperations -EA Ignore) { $PendingRebootStatus=$true }
try {
$util = [wmiclass]"\\.\root\ccm\clientsdk:CCM_ClientUtilities"
$status = $util.DetermineIfRebootPending()
if(($status -ne $null) -and $status.RebootPending)
{
$PendingRebootStatus = $true
}
}
catch{}
}
if ($PendingRebootStatus -eq 'True')
{
$Result = 'Pending restart'
}
$details = New-Object PSObject
$details | Add-Member -MemberType NoteProperty -Name Date -Value $_.Date
$details | Add-Member -MemberType NoteProperty -Name Title -Value $Title
$details | Add-Member -MemberType NoteProperty -Name Status -Value $Result
$details
} | Sort-Object -Descending:$true -Property Date
} else {
Get-Hotfix | Sort-Object InstalledOn,HotFixID -Descending | ForEach-Object -Process {
$Title = $null
$Title = $_.Description + ' - ' + $_.HotFixID
$Result = $null
$Result = 'No Status when using Get-Hotfix'
$details = New-Object PSObject
$details | Add-Member -MemberType NoteProperty -Name Date -Value $_.InstalledOn
$details | Add-Member -MemberType NoteProperty -Name Title -Value $Title
$details | Add-Member -MemberType NoteProperty -Name Status -Value $Result
$details
}
}
#Select-Object -Property * -ExcludeProperty Name | Format-Table -AutoSize -Wrap
$details | Where-Object {$_.PSComputerName -ne $null} | Select-Object -Property Date, Status, Title #| Format-List | Out-String).Trim()
#} -ArgumentList $KBNumber_or_TitleString
} -ErrorAction SilentlyContinue
#$AllServerUpdates.Count
#$AllServerUpdates | Where-Object {$_.Title -like "*$KBNumber_or_TitleString*"}
$ServerUpdates = $AllServerUpdates | Where-Object {$_.Title -like "*$KBNumber_or_TitleString*"}
if ($AllServerUpdates -eq $null)
{
$KB = New-Object -TypeName PSObject
$KB | Add-Member -MemberType NoteProperty -Name ServerName -Value *$svr
$KB | Add-Member -MemberType NoteProperty -Name PatchStatus -Value 'Cannot Access Server'
$KB | Add-Member -MemberType NoteProperty -Name PatchDate -Value '-----'
$KB | Add-Member -MemberType NoteProperty -Name PatchName -Value '-----'
$KBs += $KB
}
if ($ServerUpdates -ne $null)
{
$counter = 0
foreach ($ServerUpdate in $ServerUpdates)
{
$KB = New-Object -TypeName PSObject
if ($counter -eq 0)
{
$KB | Add-Member -MemberType NoteProperty -Name ServerName -Value $svr
} else {
$KB | Add-Member -MemberType NoteProperty -Name ServerName -Value ''
}
$counter++
$KB | Add-Member -MemberType NoteProperty -Name PatchStatus -Value $ServerUpdate.Status
$KB | Add-Member -MemberType NoteProperty -Name PatchDate -Value $ServerUpdate.Date
$KB | Add-Member -MemberType NoteProperty -Name PatchName -Value $ServerUpdate.Title
$KBs += $KB
}
} else {
$LatestServerUpdateDate = $AllServerUpdates | Select-Object -First 1 Date | Get-Date -Hour 0 -Minute 0 -Second 0
#$LatestServerUpdateDate
$ServerUpdates = $AllServerUpdates | Where-Object {$_.Date -ge $LatestServerUpdateDate}
#$ServerUpdates.Count
#$ServerUpdates
$counter = 0
foreach ($ServerUpdate in $ServerUpdates)
{
$KB = New-Object -TypeName PSObject
if ($counter -eq 0)
{
$KB | Add-Member -MemberType NoteProperty -Name ServerName -Value *$svr
} else {
$KB | Add-Member -MemberType NoteProperty -Name ServerName -Value ''
}
$counter++
$KB | Add-Member -MemberType NoteProperty -Name PatchStatus -Value $ServerUpdate.Status
$KB | Add-Member -MemberType NoteProperty -Name PatchDate -Value $ServerUpdate.Date
$KB | Add-Member -MemberType NoteProperty -Name PatchName -Value $ServerUpdate.Title
$KBs += $KB
}
}
}
$KBs | ft -AutoSize -Wrap
read-host “Press ENTER to continue...”