0

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..."
wazzie
  • 23
  • 1
  • 8
  • does `Win32_QuickFixEngineering` show those KBs when run locally? my understanding is that the QFE stuff is NOT all-inclusive. i can't recall what is included, tho. [*blush*] – Lee_Dailey Aug 09 '19 at 19:11
  • ahh, it looks like I'm ONLY able to run the Get-Wmiobject -class Win32_QuickFixEngineering locally, and not on the remote machines – wazzie Aug 09 '19 at 21:48

2 Answers2

1

I would like to say that there is apparently a misconception about the ability to obtain information about all installed patches from Win32_QuickFixEngineering WMI class. Even the official documentation states:

Updates supplied by Microsoft Windows Installer (MSI) or the Windows update site (https://update.microsoft.com) are not returned by Win32_QuickFixEngineering.

It seems that Win32_QuickFixEngineering is something like old fashioned approach which should be re replaced by using Windows Update Agent API to enumerate all updates installed using WUA - https://learn.microsoft.com/en-us/windows/win32/wua_sdk/using-the-windows-update-agent-api

Also, please take a loot at this good article - https://support.infrasightlabs.com/article/what-does-the-different-windows-update-patch-dates-stand-for/

You will find a lot of code examples by searching by "Microsoft.Update.Session" term

mklement0
  • 382,024
  • 64
  • 607
  • 775
Kostia Shiian
  • 1,024
  • 7
  • 12
0

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
Theo
  • 57,719
  • 8
  • 24
  • 41
  • Great, thanks for this. I was able to incorporate this into my script using the methods that you displayed. – wazzie Aug 13 '19 at 16:40
  • @wazzie Glad to have helped! – Theo Aug 15 '19 at 18:51
  • One more question. Is there a way to change it so I can search for all install hotfixes by the Date Range. Say for example, I add two more parameters ($startDate, $endDate) and search by Get-HotFix | Where { $_.InstalledOn -ge $using:StartDate -AND $_.InstalledOn -le $using:EndDate } | Select-Object -ExpandProperty HotFixID ? – wazzie Aug 19 '19 at 17:29
  • @wazzie I have updated my answer with new code that will enable you to do just that. Cheers! – Theo Aug 20 '19 at 19:59
  • Thanks Theo. It's working now, but I am trying to match the output exactly like the one from the original question. Basically listing it out the 'Computer', 'LastBootupTime', 'UpdateID' all the same line when searching via the date-range. – wazzie Aug 21 '19 at 20:47
  • @wazzie you could pipe it through by adding `| Format-Table -AutoSize` at the end. Mind you, the strings may be truncated in the display by PS to fit the console width.. – Theo Aug 21 '19 at 21:12
  • perfect, this is exactly what I wanted. Thank you Theo for all your help. – wazzie Aug 21 '19 at 22:11