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 😦
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) }
NB: This is very slow
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})'
$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.
Voilà 🙂
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):
Great Script! How do I make the date and time to be export to my timezone format (EST)?
awesome Work! 🙂
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:
So that you’ll have:
you are God sent. that did the trick! thank you very much! 🙂
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:
You add the ComputerName like this:
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
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.
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 ;-).
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
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.
Excellent!
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.
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.
how to hide windows failed update history from view update history result….
kindly help..
Just change Line 33. From:
} | Sort-Object -Descending:$true -Property InstalledOn |
To
} | Where { $_.Status -eq “Succeeded” } | Sort-Object -Descending:$true -Property InstalledOn |
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]
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.
Pingback: Powershell, How to get date of last Windows update install or at least checked for an update? - ErrorsFixing
Pingback: Powershell, How to get date of last Windows update install or at least checked for an update?
Hello Emin, how to add installedby to know who installed the update on this? Appreciate your help!
Hello,
Unfortunately, there’s no ‘InstalledBy’ property when the Windows Update Agent COM Object is being used to search of the history of installed updates.
You can find the InstalledBy property if you query the WMI class win32-quickfixengineering
https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-quickfixengineering
You can either use the built-in Get-Hotfix cmdlet or Get-CimInstance Win32_QuickFixEngineering
Thank you Emin for your response. Is there any other powershell command to get installed OS updates, security and cumulative patches? Appreciate your response.
Yes, there is. You can use the dism.exe built-in cmd or its PowerShell cmdlet