Getting Windows Updates installation history

Well, you probably know that there’s a Get-Hotfix cmdlet to query the list of installed updates on computer. However it has a huge drawback as you cannot determine when they have been installed as their WMI InstallDate property is empty 😦
Get-Hotfix output type

There are other ways to get this information. Let’s examine what is the best approach.

  • From eventlog entries
  • Get-WinEvent -FilterHashtable @{LogName = "System";ID=19,20} | ForEach-Object -Process {            
        Get-EventLog -Index ($_.RecordID)  -LogName $($_.LogName)            
    }

    Get WU install info from event logs
    NB: This is very slow

  • From Windowsupdate.log
    We could use the ColorWU script I proposed on this page with the following parameter -SearchString “Successfully installed”
    NB: However it’s not exhaustive and you’ve to parse again the log for failures
    Or
    We could use the Get-Maches function I borrowed from Herr Dr. Tobias Weltner on this page and that I presented when analysing WordPress stats

    Get-Content $env:windir\windowsupdate.log -Encoding UTF8 -ReadCount 0 |             
    Get-Matches '(?\d{4}-\d{2}-\d{2}).*?(?\d{2}:\d{2}:\d{2}).*?successfully installed.*?update: (?.*?) \({0,1}KB(?\d{5,8})'

    NB: This the fastest way of parsing WindowsUpdate.log but the results are not exhaustive.
    You also have to parse the log for errors like this:

    Get-Content $env:windir\windowsupdate.log -Encoding UTF8 -ReadCount 0 |            
    Get-Matches '(?\d{4}-\d{2}-\d{2}).*?(?\d{2}:\d{2}:\d{2}).*?failed to install.*?update with error (?0x\d{8}): (?.*?) \({0,1}KB(?\d{5,8})'

    Parse WindowsUpdate for failures

  • Using the Windows Update Agent COM Object
  • $Session = New-Object -ComObject Microsoft.Update.Session            
    $Searcher = $Session.CreateUpdateSearcher()            
    $HistoryCount = $Searcher.GetTotalHistoryCount()            
    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa386532%28v=vs.85%29.aspx            
    $Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {            
        $Title = $null            
        if($_.Title -match "\(KB\d{6,7}\)"){            
            # Split returns an array of strings            
            $Title = ($_.Title -split '.*\((?KB\d{6,7})\)')[1]            
        }else{            
            $Title = $_.Title            
        }            
        # http://msdn.microsoft.com/en-us/library/windows/desktop/aa387095%28v=vs.85%29.aspx            
        $Result = $null            
        Switch ($_.ResultCode)            
        {            
            0 { $Result = 'NotStarted'}            
            1 { $Result = 'InProgress' }            
            2 { $Result = 'Succeeded' }            
            3 { $Result = 'SucceededWithErrors' }            
            4 { $Result = 'Failed' }            
            5 { $Result = 'Aborted' }            
            default { $Result = $_ }            
        }            
        New-Object -TypeName PSObject -Property @{            
            InstalledOn = Get-Date -Date $_.Date;            
            Title = $Title;            
            Name = $_.Title;            
            Status = $Result            
        }            
                
    } | Sort-Object -Descending:$true -Property InstalledOn |             
    Select-Object -Property * -ExcludeProperty Name | Format-Table -AutoSize -Wrap

    NB: This one is exhaustive and damn fast.
    History from WUA

Voilà 🙂

24 thoughts on “Getting Windows Updates installation history

  1. I’ve just noticed that the some code is missing when I copied/pasted the result of the Write-ColorizedHTML ISE add-on.
    Here’s the correct code (line 9 was the culprit):

    $Session = New-Object -ComObject Microsoft.Update.Session
    $Searcher = $Session.CreateUpdateSearcher()
    $HistoryCount = $Searcher.GetTotalHistoryCount()
    # http://msdn.microsoft.com/en-us/library/windows/desktop/aa386532%28v=vs.85%29.aspx
    $Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
        $Title = $null
        if($_.Title -match "\(KB\d{6,7}\)"){
            # Split returns an array of strings
            $Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
        }else{
            $Title = $_.Title
        }
        # http://msdn.microsoft.com/en-us/library/windows/desktop/aa387095%28v=vs.85%29.aspx
        $Result = $null
        Switch ($_.ResultCode)
        {
            0 { $Result = 'NotStarted'}
            1 { $Result = 'InProgress' }
            2 { $Result = 'Succeeded' }
            3 { $Result = 'SucceededWithErrors' }
            4 { $Result = 'Failed' }
            5 { $Result = 'Aborted' }
            default { $Result = $_ }
        }
        New-Object -TypeName PSObject -Property @{
            InstalledOn = Get-Date -Date $_.Date;
            Title = $Title;
            Name = $_.Title;
            Status = $Result
        }
    
    } | Sort-Object -Descending:$true -Property InstalledOn | 
    Select-Object -Property * -ExcludeProperty Name | Format-Table -AutoSize -Wrap
    
      • Hi,
        Thx.
        Good question. Actually, the custom property InstalledOn returned is a Datetime object. To convert it to local time (whatever timezone you’re in) you can insert in the last line the following piece of code:

        ,@{l='local time';e={$_.InstalledOn.ToLocalTime()}}
        

        So that you’ll have:

        Select-Object -Property *,@{l='local time';e={$_.InstalledOn.ToLocalTime()}} -ExcludeProperty Name | Format-Table -AutoSize -Wrap
        
  2. Thanks! Great script and very handy! Is it possible to get the computername listed on the results?
    How to run this the script to all servers in your Active Directory?

    • Hi,
      Yes, this is possible to add the computername on the results.
      All you need to do is to add a new property to the new-object returned.

      Instead of the above code reproduced here:

      New-Object -TypeName PSObject -Property @{
              InstalledOn = Get-Date -Date $_.Date;
              Title = $Title;
              Name = $_.Title;
              Status = $Result
      }     
      

      You add the ComputerName like this:

      New-Object -TypeName PSObject -Property @{
              InstalledOn = Get-Date -Date $_.Date;
              Title = $Title;
              Name = $_.Title;
              Status = $Result
              ComputerName = $env:COMPUTERNAME
      }
      

      To run this the script on all servers in your Active Directory, I would first use an ActiveDirectory cmdlet to extract this list.
      Then I would copy/paste the above code into a script block and do something like this

      # 1. Define credentials
      $cred = Get-Credential
      # 2. Define a scriptblock
      $sb = {
          # remove the last pipe with the ft cmdlet, i.e.:
          # | Format-Table -AutoSize -Wrap
      
          $Session = New-Object -ComObject Microsoft.Update.Session
          $Searcher = $Session.CreateUpdateSearcher()
          $HistoryCount = $Searcher.GetTotalHistoryCount()
      
          $Searcher.QueryHistory(0,$HistoryCount) | ForEach-Object -Process {
              $Title = $null
              if($_.Title -match "\(KB\d{6,7}\)"){
                  # Split returns an array of strings
                  $Title = ($_.Title -split '.*\((?<KB>KB\d{6,7})\)')[1]
              }else{
                  $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 = $_ }
              }
              New-Object -TypeName PSObject -Property @{
                  InstalledOn = Get-Date -Date $_.Date;
                  Title = $Title;
                  Name = $_.Title;
                  Status = $Result
              }
          } | 
          Sort-Object -Descending:$false -Property InstalledOn | 
          Where { $_.Title -notmatch "^Definition\sUpdate" }
      
      }
      # 3. Get all servers in your AD (if there are less than 10000)
      Get-ADComputer -ResultPageSize 10000 -SearchScope Subtree  -Filter {
          (OperatingSystem -Like "Windows*Server*")
      } | ForEach-Object {
          # Get the computername from the AD object
          $computer = $_.Name
          # Create a hash table for splatting
          $HT = @{
              ComputerName = $computer ;
              ScriptBlock = $sb ;
              Credential = $cred;
              ErrorAction = "Stop";
          }
          # Execute the code on remote computers
          try {
              Invoke-Command @HT
          } catch {
              Write-Warning -Message "Failed to execute on $computer because $($_.Exception.Message)"
          }
      } | Format-Table PSComputerName,Title,Status,InstalledOn,Name -AutoSize 
      

      PS: This is based on the assumption that you’ve PSRemoting enabled and configured on all your servers, that you’ve less than 10000 servers and have ActiveDirectory cmdlets.

      PS: If you execute the code using Invoke-Command, there’s a PSComputerName propery automatically appended to all the results

      • Please help to execute above script against a set of computers instead of all servers in AD. Also, please help to check if specific MS KB# is installed in a list of servers.

  3. Thanks! You’re brilliant. I will give it a try to do it on all AD computers. I am not a good scripting guy so it could be that i need your help again ;-).

  4. I have put the following code in the script block:

    Import-Module ActiveDirectory
    Get-ADComputer -Filter * -SearchBase “OU=yourOU,DC=yourdomain,DC=local” | %{ $_.DNSHostName }

    The script runs but it prompts with
    WARNING: Failed to execute on because connecting to remote server failed with the following error message: The client cannot connect to the destination specified in the request. Verify that the service on the destination is running and accepting requests.

    PSRemoting is enabled on the computers.
    Can you help?

    • Yes, I can help,

      It seems that error

      The client cannot connect to the destination specified in the request. Verify that the service on the destination is running and accepting requests.

      is well known and that it’s referenced in the built-in help of PowerShell

      The help actually says:

      I’d recommend that you go onto this page and read the whole paragraph about this error:
      http://technet.microsoft.com/en-us/library/hh847850.aspx
      It seems to be a good starting point to troubleshoot WinRM in your environment.

  5. Hello,

    Could you please help to execute the above script against a specific set of servers instead of all servers in AD ? Also, please help to check if specific MS KB# is installed in a list of servers.

  6. Hi Emin, fantastic Script. I’ve found very useful and interesting.
    I’m not a scripting guy or even programmer, so like many I search for scripts on the internet that can work for me.
    About this Script do you have a version as a “Function”?? Could be used inside others and more easy to point for a list $computers = Get-Content “c:\scripts\dclist.txt”.
    If you have it please share with us.

    • Just change Line 33. From:
      } | Sort-Object -Descending:$true -Property InstalledOn |

      To

      } | Where { $_.Status -eq “Succeeded” } | Sort-Object -Descending:$true -Property InstalledOn |

  7. Thank you very much, that was very helpful.

    I also came across that error in line 9 of the powershell and found a pretty easy fix: Just capture the KB number in match phase, then use the matched string directly like this:

    if($_.Title -match “\((KB\d{6,7})\)”){
    $Title = $matches[1]

  8. Hi
    Why it’s showing only 27 updates when there 165 updates for Windows only listed in Control panel – Programs and Features, please?

    • Hi,
      The code that queries the history using the Microsoft.Update.Session COM Object does it using the local database located in C:\windows\SoftwareDistribution.
      The control panel applet also queries the registry in various location.

      For example, if you did some Windows Update troubleshooting and renamed or deleted the C:\windows\softwaredistribution folder, you’ve actually deleted the history of stored in the database that was located under this folder. The WU troubleshooter doesn’t delete the history stored in the registry.

      I consider the data pulled using the Microsoft.Update.Session COM Object as volatile and non exhaustive.

  9. Pingback: Powershell, How to get date of last Windows update install or at least checked for an update? - ErrorsFixing

  10. Pingback: Powershell, How to get date of last Windows update install or at least checked for an update?

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.