top of page

Report Windows Updates with PowerShell

  • Feb 15
  • 3 min read

Windows includes few tools to report Windows Updates using PowerShell like Get-HotFix and Microsoft.Update.Session COM object. However, each seems to be missing some info - Get-HotFix only includes Security Updates and doesn't include hour:minute:second information which is critical in many scenarios; Microsoft.Update.Session often fails without clear solution provided). Below script combines few various methods to ensure no update is missed in the report. Feel free to just use one of the methods. The 'Get-WindowsPackage' method in combination with MUM info is a result of bunch of reverse engineering (involving Kernel trace) so it's a bit of pride for our Team since there doesn't seem to be anything like it (or about it) on the Internet at the moment.


The output will be saved as $SecurityPatches variable since the code is a bit raw. Feel free to wrap it into a module. Hope you find it useful.


$SecurityPatches   = @()
$OS = Get-CimInstance win32_operatingsystem
$ErrorActionPreference = "SilentlyContinue"  
$Session = New-Object -ComObject "Microsoft.Update.Session"
$Searcher = $Session.CreateUpdateSearcher()
$historyCount = $Searcher.GetTotalHistoryCount()
$Results = $Searcher.QueryHistory(0, $historyCount) | Microsoft.PowerShell.Core\Where-Object { $_.Operation -ne $null }
foreach ($result in $results) 
     {
   $Info = "" | Select HotFixID,Title,InstalledOn,Status,'User/Application',Product   $info.HotFixID = IF($result.Title.tostring() -match "\(KB.*?\)" -OR $result.Title.tostring() -match "\ KB.*?\ "){$matches[0].replace('(','').replace(')','')}
$info.Title = $result.Title
$info.InstalledOn = (([datetime]$result.Date).ToLocalTime()).ToString('yyyy/MM/dd HH:mm:ss')   $info.Status = switch($result.resultcode)                                                    {
                                                        0 {"Not Started"};                                                        1 {"In Progress"};                                                        2 {"Installed"};                                                        3 {"Installed With Errors"};                                                        4 {"Failed"};                                                        5 {"Aborted"}                                                    },   $info.'User/Application' = $result.ClientApplicationID

if ($OS.caption -notlike "*2012 R2*")
      {
       $info.Product = ($Result.Categories | Select Name).Name
     }
    $SecurityPatches += $Info
}

foreach ($package in (Get-WindowsPackage -Online))
           {
            $item = "" | Select HotFixID,Title,InstalledOn,Status,'User/Application',Product           $MumInfo = ([xml](Get-content -Path "C:\Windows\servicing\Packages\$($package.PackageName).mum" -ea SilentlyContinue)).assembly
$item.HotFixID = if ($package.PackageName -like "Package_for_KB*") {$package.PackageName.split('_')[2].split('~')[0]}                             elseif ($MumInfo.package.identifier.tostring() -match "KB\d+" ) {$matches[0]}                             
else {try {$MumInfo.description.Split(' ') | ? {$_ -like "KB*"}} catch {}} 

$item.Title = if ($item.HotFixID) {switch ($package.ReleaseType)                                   {                                    
SecurityUpdate {"Security Update for Microsoft Windows (" + $item.HotFixID + ")"}
                                    Update {"Update for Microsoft Windows (" + $item.HotFixID + ")"}
                                    Default {                                               if ($muminfo.description) {$muminfo.description}                                              else {$package.PackageName}
                                            }                                   }
                            }
                          elseif ($muminfo.description) {$muminfo.description}
                          else {$package.PackageName}            $item.Status = $package.PackageState
            if ($OS.caption -like "*2019*")
              {
               $item.InstalledOn = (([datetime]$package.InstallTime).ToLocalTime()).ToString('yyyy/MM/dd HH:mm:ss')             
 }
         else {$item.InstalledOn = (get-date (([datetime]$package.InstallTime).ToLocalTime())).AddHours(-12).ToString('yyyy/MM/dd HH:mm:ss')}
            $item.Product = "Microsoft Windows"
         $SecurityPatches += $item
           } # end foreach package

foreach ($UserData in (gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData"))      {
       foreach ($Product in (gci "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\$($UserData.PSChildName)\Products" -ErrorAction SilentlyContinue))
            {
             foreach ($PatchSQUID in (gci Registry::$($Product.Name)\Patches))
                  {
                    $patch_Info = Get-ItemProperty Registry::$($PatchSQUID.name)
                    if ($patch_Info.State -ne "2")
                     {
                       $item = "" | Select HotFixID,Title,InstalledOn,Status,'User/Application',Product   

$item.HotFixID = IF($patch_Info.displayname.tostring() -match "\(KB.*?\)" -OR $patch_Info.displayname.tostring() -match "\ KB.*?\ " -OR $patch_Info.displayname.tostring() -match "KB\d+"){$matches[0].replace('(','').replace(')','')}                       $item.Title = $patch_Info.displayname                       $item.InstalledOn = ([DateTime]$([DateTime]::ParseExact("$($patch_Info.Installed)",'yyyyMMdd',$null))).ToString('yyyy/MM/dd HH:mm:ss')                       $item.Status  = switch ($patch_Info.State) {                                                                  1 {"Installed"; Break}                                                                  2 {"Superseded"; Break}                                                                  default {"Unknown"; Break}                                                                 }
                       $user = ((New-Object System.Security.Principal.SecurityIdentifier($UserData.PSChildName)).Translate([System.Security.Principal.NTAccount])).Value                       $item.'User/Application' = if ($user) {$user} else {$UserData.PSChildName}                       $item.Product =  (Get-ItemProperty Registry::$($Product.Name)\InstallProperties).DisplayName                       $SecurityPatches += $item
                     } # end if State
                  } # end foreach patch
            } # end foreach product
      } # end foreach userdata
$ErrorActionPreference = "SilentlyContinue"    

$w32 = gp -Path HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\* | ? {$_.displayname -like "*update*(KB*" -OR $_.displayname -like "*Service Pack*(KB*" -OR $_.Displayname -like "*Hotfix*(KB*"} | select Displayname,@{N="HotFixID"; E={'KB' + ($_.DisplayName.Split('(KB').split(')') | ? {$_ -match "^\d+$"})}},InstallDate
$w = gp -Path HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\* | ? {$_.displayname -like "*update*(KB*" -OR $_.displayname -like "*Service Pack*(KB*" -OR $_.Displayname -like "*Hotfix*(KB*"}| select Displayname,@{N="HotFixID"; E={'KB' + ($_.Displayname.Split('(KB').split(')') | ? {$_ -match "^\d+$"})}},InstallDate
$results = $w32 + $w
foreach ($result in $results) 
       {
        if ($SecurityPatches.HotFixID -notcontains $result.HotFixID)           {
           $Info = "" | Select HotFixID,Title,InstalledOn,Status,Product
           $info.HotFixID = $result.HotFixID
           $info.Title = $result.Displayname
           $info.InstalledOn = ([DateTime]$result.InstallDate).ToString('yyyy/MM/dd HH:mm:ss')           $info.Operation = $null
           $info.Status = $null
           $info.Product = $result.ParentDisplayName
            $SecurityPatches += $Info
          } #end IF
        } #end foreach
$ErrorActionPreference = "Continue"
$SecurityPatches = $SecurityPatches | Sort InstalledOn,HotFixID -Descending

References:

 
 
bottom of page