Search code examples
powershellhotfix

Checking if windows security patches are installed on multiple servers


I am trying to check if the specified KB # that I have set in my variables list matches the full list of KB installed patches on the server. If it matches, it will display that the patch is installed, otherwise it will state that it is not installed.

The code below does not seem to work, as it is showing as not installed, but in fact it's already been installed.

[CmdletBinding()]

param ( [Parameter(Mandatory=$true)][string] $EnvRegion )

if ($EnvRegion -eq "kofax"){
    [array]$Computers = "wprdkofx105", 
                        "wprdkofx106", 
                        "wprdkofx107", 

              $KBList = "KB4507448",
                        "KB4507457",
                        "KB4504418"
}
elseif ($EnvRegion -eq "citrix"){
    [array]$Computers = "wprdctxw124",
                        "wprdctxw125",

              $KBList = "KB4503276",
                        "KB4503290",
                        "KB4503259",
                        "KB4503308"
}

### Checks LastBootUpTime for each server

function uptime {
    gwmi win32_operatingsystem |  Select 
    @{LABEL='LastBootUpTime';EXPRESSION= 
    {$_.ConverttoDateTime($_.lastbootuptime)}} | ft -AutoSize
}

### Main script starts here.  Loops through all servers to check if 
### hotfixes have been installed and server last reboot time

foreach ($c in $Computers) {    
Write-Host "Server $c" -ForegroundColor Cyan

### Checks KB Installed Patches for CSIRT to see if patches have been 
### installed on each server 

    foreach ($elem in $KBList) {

    $InstalledKBList = Get-Wmiobject -class Win32_QuickFixEngineering - 
    namespace "root\cimv2" | where-object{$_.HotFixID -eq $elem} | 
    select-object -Property HotFixID | Out-String
        if ($InstalledKBList -match $elem) {
            Write-Host "$elem is installed" -ForegroundColor Green
        } 
        else { 
            Write-Host "$elem is not installed" -ForegroundColor Red
        }
    }
    Write-Host "-------------------------------------------"
    Invoke-Command -ComputerName $c -ScriptBlock ${Function:uptime}
}

Read-Host -Prompt "Press any key to exit..."

Solution

  • As Kostia already explained, the Win32_QuickFixEngineering does NOT retrieve all updates and patches. To get these, I would use a helper function that also gets the Windows Updates and returns them all as string array like below:

    function Get-UpdateId {
        [CmdletBinding()]  
        Param (   
            [string]$ComputerName = $env:COMPUTERNAME
        ) 
    
        # First get the Windows HotFix history as array of 'KB' id's
        Write-Verbose "Retrieving Windows HotFix history on '$ComputerName'.."
    
        $result = Get-HotFix -ComputerName $ComputerName | Select-Object -ExpandProperty HotFixID
        # or use:
        # $hotfix = Get-WmiobjectGet-WmiObject -Namespace 'root\cimv2' -Class Win32_QuickFixEngineering -ComputerName $ComputerName | Select-Object -ExpandProperty HotFixID
    
        # Next get the Windows Update history
        Write-Verbose "Retrieving Windows Update history on '$ComputerName'.."
    
        if ($ComputerName -eq $env:COMPUTERNAME) {
            # Local computer
            $updateSession = New-Object -ComObject Microsoft.Update.Session
        }
        else {
            # Remote computer (the last parameter $true enables exception being thrown if an error occurs while loading the type)
            $updateSession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session", $ComputerName, $true))
        }
    
        $updateSearcher = $updateSession.CreateUpdateSearcher()
        $historyCount   = $updateSearcher.GetTotalHistoryCount()
    
        if ($historyCount -gt 0) {
            $result += ($updateSearcher.QueryHistory(0, $historyCount) | ForEach-Object { [regex]::match($_.Title,'(KB\d+)').Value })
        }
    
        # release the Microsoft.Update.Session COM object
        try {
            [System.Runtime.Interopservices.Marshal]::ReleaseComObject($updateSession) | Out-Null
            Remove-Variable updateSession
        }
        catch {}
    
        # remove empty items from the combined $result array, uniquify and return the results
        $result | Where-Object { $_ -match '\S' } | Sort-Object -Unique
    }
    

    Also, I would rewrite your uptime function to become:

    function Get-LastBootTime {
        [CmdletBinding()]  
        Param (   
            [string]$ComputerName = $env:COMPUTERNAME
        ) 
        try {
            $os = Get-WmiObject -Class Win32_OperatingSystem -ComputerName $ComputerName
            $os.ConvertToDateTime($os.LastBootupTime)
        } 
        catch {
            Write-Error $_.Exception.Message    
        }
    }
    

    Having both functions in place, you can do

    $Computers | ForEach-Object {
        $updates = Get-UpdateId -ComputerName $_ -Verbose
        # Now check each KBid in your list to see if it is installed or not
        foreach ($item in $KBList) {
            [PSCustomObject] @{
                'Computer'       = $_
                'LastBootupTime' = Get-LastBootTime -ComputerName $_
                'UpdateID'       = $item
                'Installed'      = if ($updates -contains $item) { 'Yes' } else { 'No' }
            }
        }
    }
    

    The output will be something like this:

    Computer     LastBootupTime    UpdateID  Installed
    --------     --------------    --------  ---------
    wprdkofx105  10-8-2019 6:40:54 KB4507448 Yes       
    wprdkofx105  10-8-2019 6:40:54 KB4507457 No       
    wprdkofx105  10-8-2019 6:40:54 KB4504418 No       
    wprdkofx106  23-1-2019 6:40:54 KB4507448 No       
    wprdkofx106  23-1-2019 6:40:54 KB4507457 Yes      
    wprdkofx106  23-1-2019 6:40:54 KB4504418 Yes 
    wprdkofx107  12-4-2019 6:40:54 KB4507448 No       
    wprdkofx107  12-4-2019 6:40:54 KB4507457 No      
    wprdkofx107  12-4-2019 6:40:54 KB4504418 Yes
    

    Note: I'm on a Dutch machine, so the default date format shown here is 'dd-M-yyyy H:mm:ss'


    Update


    In order to alse be able to select on a date range, the code needs to be altered so the function Get-UpdateId returns an array of objects, rather than an array of strings like above.

    function Get-UpdateId {
        [CmdletBinding()]  
        Param (
            [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true , Position = 0)]
            [string]$ComputerName = $env:COMPUTERNAME
        ) 
    
        # First get the Windows HotFix history as array objects with 3 properties: 'Type', 'UpdateId' and 'InstalledOn'
        Write-Verbose "Retrieving Windows HotFix history on '$ComputerName'.."
    
        $result = Get-HotFix -ComputerName $ComputerName | Select-Object @{Name = 'Type'; Expression = {'HotFix'}}, 
                                                                         @{Name = 'UpdateId'; Expression = { $_.HotFixID }}, 
                                                                         InstalledOn
        # or use:
        # $result = Get-WmiobjectGet-WmiObject -Namespace 'root\cimv2' -Class Win32_QuickFixEngineering -ComputerName $ComputerName | 
        #                Select-Object @{Name = 'Type'; Expression = {'HotFix'}}, 
        #                              @{Name = 'UpdateId'; Expression = { $_.HotFixID }},
        #                              InstalledOn
    
        # Next get the Windows Update history
        Write-Verbose "Retrieving Windows Update history on '$ComputerName'.."
    
        if ($ComputerName -eq $env:COMPUTERNAME) {
            # Local computer
            $updateSession = New-Object -ComObject Microsoft.Update.Session
        }
        else {
            # Remote computer (the last parameter $true enables exception being thrown if an error occurs while loading the type)
            $updateSession = [activator]::CreateInstance([type]::GetTypeFromProgID("Microsoft.Update.Session", $ComputerName, $true))
        }
    
        $updateSearcher = $updateSession.CreateUpdateSearcher()
        $historyCount   = $updateSearcher.GetTotalHistoryCount()
    
        if ($historyCount -gt 0) {
            $result += ($updateSearcher.QueryHistory(0, $historyCount) | ForEach-Object { 
                [PsCustomObject]@{
                    'Type'        = 'Windows Update'
                    'UpdateId'    = [regex]::match($_.Title,'(KB\d+)').Value
                    'InstalledOn' = ([DateTime]($_.Date)).ToLocalTime()
                }
            })
        }
    
        # release the Microsoft.Update.Session COM object
        try {
            [System.Runtime.Interopservices.Marshal]::ReleaseComObject($updateSession) | Out-Null
            Remove-Variable updateSession
        }
        catch {}
    
        # remove empty items from the combined $result array and return the results
        $result | Where-Object { $_.UpdateId -match '\S' }
    }
    

    The Get-LastBootTime function does not need changing, so I leave you to copy that from the first part of the answer.

    To check for installed updates by their UpdateId property

    $Computers | ForEach-Object {
        $updates = Get-UpdateId -ComputerName $_ -Verbose
        $updateIds = $updates | Select-Object -ExpandProperty UpdateId
        # Now check each KBid in your list to see if it is installed or not
        foreach ($item in $KBList) {
            $update = $updates | Where-Object { $_.UpdateID -eq $item }
            [PSCustomObject] @{
                'Computer'       = $_
                'LastBootupTime' = Get-LastBootTime -ComputerName $_
                'Type'           = $update.Type
                'UpdateID'       = $item
                'IsInstalled'    = if ($updateIds -contains $item) { 'Yes' } else { 'No' }
                'InstalledOn'    = $update.InstalledOn
            }
        }
    }
    

    Output (something like)

    Computer       : wprdkofx105
    LastBootupTime : 10-8-2019 20:01:47
    Type           : Windows Update
    UpdateID       : KB4507448
    IsInstalled    : Yes
    InstalledOn    : 12-6-2019 6:10:11
    
    Computer       : wprdkofx105
    LastBootupTime : 10-8-2019 20:01:47
    Type           : 
    UpdateID       : KB4507457
    IsInstalled    : No
    InstalledOn    :
    

    To get hotfixes and updates installed within a start and end date

    $StartDate = (Get-Date).AddDays(-14)
    $EndDate   = Get-Date
    
    foreach ($computer in $Computers) {
        Get-UpdateId -ComputerName $computer  | 
            Where-Object { $_.InstalledOn -ge $StartDate -and $_.InstalledOn -le $EndDate } |
            Select-Object @{Name = 'Computer'; Expression = {$computer}}, 
                          @{Name = 'LastBootupTime'; Expression = {Get-LastBootTime -ComputerName $computer}}, *
    }
    

    Output (something like)

    Computer       : wprdkofx105
    LastBootupTime : 20-8-2019 20:01:47
    Type           : HotFix
    UpdateId       : KB4474419
    InstalledOn    : 14-8-2019 0:00:00
    
    Computer       : wprdkofx107
    LastBootupTime : 20-8-2019 20:01:47
    Type           : Windows Update
    UpdateId       : KB2310138
    InstalledOn    : 8-8-2019 15:39:00