- Reading Azure VM name, IP address, and hostname with PowerShell - Fri, Jul 28 2017
- Automating WSUS with PowerShell - Thu, Jul 13 2017
- Disable SSL and TLS 1.0/1.1 on IIS with PowerShell - Tue, Jun 27 2017
I wrote the PowerShell script I describe in this post due to the recent unfortunate events when the WannaCry ransomware infected literally hundreds of thousands of computers. As usual, the main reason was that people didn't install security updates on time.
To check the state of all of our computers, I tried to use WSUS reporting. However, I quickly found out that the tool is not very flexible. Thus, it is extremely arduous to get the report for all machines even if you are looking for just one particular KB number. As you can see below, the report choices are pretty poor:
You need to create a report for every update of every operating system type. Imagine that you have many different Windows versions starting from Windows XP. It'll take you forever. Fortunately, you can use PowerShell to achieve your goal faster.
Function GetUpdateState { param([string[]]$kbnumber, [string]$wsusserver, [string]$port ) $report = @() [void][reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($wsusserver,$False,8530) $CompSc = new-object Microsoft.UpdateServices.Administration.ComputerTargetScope $updateScope = new-object Microsoft.UpdateServices.Administration.UpdateScope; $updateScope.UpdateApprovalActions = [Microsoft.UpdateServices.Administration.UpdateApprovalActions]::Install foreach ($kb in $kbnumber){ #Loop against each KB number passed to the GetUpdateState function $updates = $wsus.GetUpdates($updateScope) | ?{$_.Title -match $kb} #Getting every update where the title matches the $kbnumber foreach($update in $updates){ #Loop against the list of updates I stored in $updates in the previous step $update.GetUpdateInstallationInfoPerComputerTarget($CompSc) | ?{$_.UpdateApprovalAction -eq "Install"} | % { #for the current update #Getting the list of computer object IDs where this update is supposed to be installed ($_.UpdateApprovalAction -eq "Install") $Comp = $wsus.GetComputerTarget($_.ComputerTargetId)# using #Computer object ID to retrieve the computer object properties (Name, #IP address) $info = "" | select UpdateTitle, LegacyName, SecurityBulletins, Computername, OS ,IpAddress, UpdateInstallationStatus, UpdateApprovalAction #Creating a custom PowerShell object to store the information $info.UpdateTitle = $update.Title $info.LegacyName = $update.LegacyName $info.SecurityBulletins = ($update.SecurityBulletins -join ';') $info.Computername = $Comp.FullDomainName $info.OS = $Comp.OSDescription $info.IpAddress = $Comp.IPAddress $info.UpdateInstallationStatus = $_.UpdateInstallationState $info.UpdateApprovalAction = $_.UpdateApprovalAction $report+=$info # Storing the information into the $report variable } } } $report | ?{$_.UpdateInstallationStatus -ne 'NotApplicable' -and $_.UpdateInstallationStatus -ne 'Unknown' -and $_.UpdateInstallationStatus -ne 'Installed' } | Export-Csv -Path c:\temp\rep_wsus.csv -Append -NoTypeInformation } #Filtering the report to list only computers where the updates are not installed $MS17010 = "4012212","4012598","4012215","4012213","4012216","4012214","4012217","4012606","4013198","4013429" $CVE20170162 = "4015221","4015219","4015217","4015583","4015550","4015547" $CVE20170261 = "3118310","3172458","3114375" GetUpdateState -kbnumber $MS17010 -wsusserver wsus -port 8530
To simplify things a bit and enable reusing the same script in the future to produce reports for different KBs, I use a function that accepts the following parameters:
- An array of strings for the KB numbers
- A string for the WSUS server name
- A string for the WSUS port number
To be able to run this function successfully, you need the Windows Update Services MMC snap-in installed. Otherwise you can run it on the WSUS server. Please note that by default, this function connects to the WSUS server using unsecured HTTP. If you're using SSL, you have to change the $False to $True in the line that initializes the $wsus variable.
I prepare the $report variable in advance, to be able to save the results into it later, and then I load the Microsoft.Update services assembly. Next, I initialize the following three variables:
- $wsus: update services server object
- $UpdateScope: WSUS update scope (list of updates on the WSUS server)
- $CompSc: computer objects registered in WSUS
Next, I set up $UpdateScope.UpdateApprovalActions to "Install" because I'm interested only in those updates approved for installation.
I then start a foreach loop against the $kbnumber string array I intend to pass to the function. This provides all update objects that have the particular KB number in the title.
Inside the second loop, I'm using the update object's GetUpdateInstallationInfoPerComputerTarget method to get the status of the update for each computer object stored in $ComSc. To understand better how the GetUpdateInstallationInfoPerComputerTarget method works, take a look at the screenshot below:
The update status object contains the ComputerTargetId property, which is a unique GUID associated with each computer object. The GUID is the key that allows me to establish relationships between the update status objects and computer names.
In the third loop (ForEach object) I pass this GUID to the getComputerTarget method of the update service object (that the $wsus variable represents). This reads several properties of the computer object, such as computer name, IP address, and update status information. Below you can see an example of the WSUS computer object:
Next, I create a custom PowerShell object and store it in the $info variable. The properties of the $info object then contain the computer object properties and update the status properties I need for the report. The $report variable then stores an array of my $info objects.
After the loops execute, I filter the $report variable to remove all records where UpdateInstallationStatus is set to Installed, Not Applicable, and Unknown. The remaining records are the computers that require the updates with the KB numbers you specify when you call the function.
In the screenshot below you see a sample report:
At the end of the script, I added three arrays with the KB numbers that are critical for mitigating the vulnerabilities used for the recent WannaCry ransomware outbreak. I obtained information on the KB numbers from the following official sources:
Subscribe to 4sysops newsletter!
- Microsoft Security Bulletin MS17-010 - Critical
- Microsoft Security Update Guide - CVE-2017-0162
- Microsoft Security Update Guide - CVE-2017-0261
Thanks Alex for this article, it can help to improve the Wsus reporting and investigation if and where is correctly applied a MS patch.
Hi Alex
Nice script! This is exactly what i’m looking for!
I had some issues running this script.
Could you provide me an explanation on how to use the script?
Do you need to run this script on the WSUS server or can you do this remotely
You either need to run it on the WSUS server or on the server which has WSUS management console installed.
hi when I run the script I get this error :
Exception calling “GetUpdateServer” with “3” argument(s): “The underlying connection was closed: An unexpected error occurred on a receive.”
At line:8 char:1
+ $wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpda …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : WebException
You cannot call a method on a null-valued expression.
At line:13 char:4
+ $updates = $wsus.GetUpdates($updateScope) | ?{$_.Title -match $kb} …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
You cannot call a method on a null-valued expression.
At line:13 char:4
+ $updates = $wsus.GetUpdates($updateScope) | ?{$_.Title -match $kb} …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : InvokeMethodOnNull
any ideas why?
I’m using https (8531) and I changed this line :
getUpdateServer($wsusserver,$true,8531)
any ideas how can I fix it ?thanks
If you are using self-signed certificate for your wsus server SSL please make sure that this certificate is added to the “Trusted Certificate Authorities” in the Certificates of the computer you’re running this script from.
If the certificate you are using is from trusted CA, make sure that CAs certificate(s) are trusted (exist in the Trusted Certificate Authorities of the computer you’re running this script from).
Hi Alex, I’m telling you that I also have the same error as Luciano, but in my case, according to your comment, I don’t use a signature certificate on my server. ?
Hello Alex,
first of all thanks for your work!
I’ve pulled your code, saved it as .ps1 and ran on the WSUS Upstream Server with the elevated rights.
at the end of the code you call your function and all looks good sofar, however genereated report file is just empty :-/ there is no single line… even no headers.
and it doesn’t give any errors back. Any Idea what it could be?
Thanks in Advance!
If report file is empty it means that the loop which gets the updates info doesn’t do its job. So’ Id check that updates with KB numbers assigned to the arrays in the lines 35, 36, 37 are downloaded to WSUS and approved.
I would like to fill the $MS17010 = “4012212”,”4012598″,”4012215″,”4012213″,”4012216″,”4012214″,”4012217″,”4012606″,”4013198″
line from a text file
How would i do this ?
What if the computers are reporting to a downstream replica WSUS? On an upstream master WSUS, the report only reports computers contacting this server. Thanks.
Hello Alex,
Great job, but how can I use it to have the report of my 2 WSUS servers (upstream+downstream)?
When I ran it from th upstream I got only results about machines linked to it, in my case 2. and when I ran from the downstream server i got only machines linked to it too.
Thansk for your help
Hi Christian,
I am seeing the same behaviour – did you ever get a reply / figure this out?
We have around 15 downstream servers so i wold have to run this script for each of them which would be slower than using WSUS itself to report.
..Charlie.
Hi Charlie,
Looks like this is all being done using the Microsoft.UpdateServices.Administration API (https://msdn.microsoft.com/en-us/library/microsoft.updateservices.administration(v=vs.85).aspx)
If you dig through the classes, there’s options to include downstream computer targets (part of the ComputerTargetScope class). You’d have to create a ComputerTargetScope object and set the IncludeDownstreamComputerTargets. I believe that to get this to work, the downstream replicas would have to be have Reporting Rollup set, but haven’t tested without rollup as it’s the default.
$CompSc = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$CompSc.IncludeDownstreamComputerTargets = $true
Adding this ‘filtering’ to you the original script should include those downstream clients.
..Tom
@olaf,
Put the values (“4012212″,”4012598″,”4012215″,”4012213″,”4012216″,”4012214″,”4012217″,”4012606″,”4013198″,”4013429”) into a text file, no headers, no footers, no quotes, no commas, and one item per line – for instance, c:\data\ms17010.txt
Change this:
$MS17010 = “4012212”,”4012598″,”4012215″,”4012213″,”4012216″,”4012214″,”4012217″,”4012606″,”4013198″,”4013429″
Then do this instead:
$MS17010 = get-content c:\data\ms17010.txt
Kurt
Hi Alex,
Great work on this script. i’ve implemented this recently to look for an Outlook Security update that broke some functionality with Enterprise Vault.
I’m looking for a way to automate the Synchronization Report that will give the result for the last 30 days. Do you know how to accomplish that?
Thanks.
You can user this script
too create a compliance report with the csv files generated.
$Date = (Get-Date -f yyyyMMdd)
$path = “C:\script\wsus\compliance\convert\*”
$csvs = Get-ChildItem $path -Include *.csv
$y=$csvs.Count
Write-Host “Detected the following CSV files: ($y)”
foreach ($csv in $csvs)
{
Write-Host ” “$csv.Name
}
$outputfilename = “c:\script\wsus\compliance\convert\updatestatus-update-$Date.xlsx”
Write-Host Creating: $outputfilename
$excelapp = new-object -comobject Excel.Application
$excelapp.sheetsInNewWorkbook = $csvs.Count
$xlsx = $excelapp.Workbooks.Add()
$sheet=1
foreach ($csv in $csvs)
{
$row=1
$column=1
$worksheet = $xlsx.Worksheets.Item($sheet)
$worksheet.Name = $csv.Name
$file = (Get-Content $csv.PSPath)
foreach($line in $file)
{
$linecontents=$line -split ‘,(?!\s*\w+”)’
foreach($cell in $linecontents)
{
$worksheet.Cells.Item($row,$column) = $cell
$column++
}
$column=1
$row++
}
$sheet++
}
$xlsx.SaveAs( $outputfilename)
$excelapp.quit()
Start-Sleep -s 300
del *.csv
send-MailMessage -SmtpServer {mailserver} -To {mailadres receiver}, {mailadres receiver} -From {mailadres sender} -Subject “rapportage monthly install status” -Body “rapportage monthly install status” -Attachment “c:\script\wsus\compliance\convert\updatestatus-update-$Date.xlsx”
Move-Item C:\script\wsus\compliance\convert\*.xlsx C:\script\wsus\reports\
hello all
i am beginner of the poweshell and wsus, i got the error when run the script. since i can success once but after restart the powershell and run again the error occur, please advice, many thanks
ERROR SCREEN
PS C:\Users\Administrator> C:\scripts\wsusSearchReport.ps1
Exception calling “GetUpdates” with “1” argument(s): “The operation has timed out”
At C:\scripts\wsusSearchReport.ps1:13 char:4
+ $updates = $wsus.GetUpdates($updateScope) | ?{$_.Title -match $kb} …
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : WebException
COMMAND SCRIPT
Function GetUpdateState {
param([string[]]$kbnumber,
[string]$wsusserver,
[string]$port
)
$report = @()
[void][reflection.assembly]::LoadWithPartialName(“Microsoft.UpdateServices.Administration”)
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer($wsusserver,$False,80)
$CompSc = new-object Microsoft.UpdateServices.Administration.ComputerTargetScope
$updateScope = new-object Microsoft.UpdateServices.Administration.UpdateScope;
$updateScope.UpdateApprovalActions = [Microsoft.UpdateServices.Administration.UpdateApprovalActions]::Install
foreach ($kb in $kbnumber){ #Loop against each KB number passed to the GetUpdateState function
$updates = $wsus.GetUpdates($updateScope) | ?{$_.Title -match $kb} #Getting every update where the title matches the $kbnumber
foreach($update in $updates){ #Loop against the list of updates I stored in $updates in the previous step
$update.GetUpdateInstallationInfoPerComputerTarget($CompSc) | ?{$_.UpdateApprovalAction -eq “Install”} | % { #for the current update
#Getting the list of computer object IDs where this update is supposed to be installed ($_.UpdateApprovalAction -eq “Install”)
$Comp = $wsus.GetComputerTarget($_.ComputerTargetId)# using #Computer object ID to retrieve the computer object properties (Name, #IP address)
$info = “” | select UpdateTitle, LegacyName, SecurityBulletins, Computername, OS ,IpAddress, UpdateInstallationStatus, UpdateApprovalAction #Creating a custom PowerShell object to store the information
$info.UpdateTitle = $update.Title
$info.LegacyName = $update.LegacyName
$info.SecurityBulletins = ($update.SecurityBulletins -join ‘;’)
$info.Computername = $Comp.FullDomainName
$info.OS = $Comp.OSDescription
$info.IpAddress = $Comp.IPAddress
$info.UpdateInstallationStatus = $_.UpdateInstallationState
$info.UpdateApprovalAction = $_.UpdateApprovalAction
$report+=$info # Storing the information into the $report variable
}
}
}
#$report | ?{$_.UpdateInstallationStatus -ne ‘NotApplicable’ -and $_.UpdateInstallationStatus -ne ‘Unknown’ -and $_.UpdateInstallationStatus -ne ‘Installed’ } | Export-Csv -Path c:\temp\rep_wsus.csv -Append -NoTypeInformation
$report | ?{$_.UpdateInstallationStatus -ne ‘NotApplicable’ -and $_.UpdateInstallationStatus -ne ‘Unknown’ } | Export-Csv -Path c:\temp\rep_wsus.csv -Append -NoTypeInformation
} #Filtering the report to list only computers where the updates are not installed
#$MS17010 = “4012212”,”4012598″,”4012215″,”4012213″,”4012216″,”4012214″,”4012217″,”4012606″,”4013198″,”4013429″
$MS17010 = “4041693”
$CVE20170162 = “4015221”,”4015219″,”4015217″,”4015583″,”4015550″,”4015547″
$CVE20170261 = “3118310”,”3172458″,”3114375″
GetUpdateState -kbnumber $MS17010 -wsusserver wsus02 -port 80
Hi All,
Thanks for the script,
“Installed and No status computers details are not exporting, Could you tell us where can we made the change in the script.
Thank You
With psexcel you can easily create a readable excel report with the csvs generated
{$Date = (Get-Date -f yyyyMMddHHmm)
Import-Module PSExcel
Import-Csv C:\script\wsus\compliance\convert\updates_rollup.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates_rollup
Import-Csv C:\script\wsus\compliance\convert\updates_office_2013.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates_office_2013
Import-Csv C:\script\wsus\compliance\convert\updates_office_2010.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates_office_2010
Import-Csv C:\script\wsus\compliance\convert\updates_office_2016.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates_office_2016
Import-Csv C:\script\wsus\compliance\convert\updates-Windows-2016.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates-Windows-2016
Import-Csv C:\script\wsus\compliance\convert\updates-server-software.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates-server-software
Import-Csv C:\script\wsus\compliance\convert\updates_ie11_flashplayer.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName updates_ie11_flashplayer
Import-Csv C:\script\wsus\compliance\convert\Dot_Net.csv | Export-XLSX -Path C:\script\wsus\compliance\convert\$Date-Updates.xlsx -WorksheetName Dot_Net
Start-Sleep -s 60
New-Item -Path C:\script\wsus\reports\$Date-csv-files -ItemType directory
New-Item -Path C:\script\wsus\reports\$Date-Compliance-Report -ItemType directory
send-MailMessage -SmtpServer {mail server} -To {receiver adress} -From {sender address} -Subject “rapportage monthly install status” -Body “rapportage monthly install status” -Attachment “c:\script\wsus\compliance\convert\$Date-Updates.xlsx”
Move-Item -Force “C:\script\wsus\compliance\convert\*.xlsx” “C:\script\wsus\reports\$Date-Compliance-Report\”
Move-Item -Force “C:\script\wsus\compliance\convert\*.csv” “C:\script\wsus\reports\$Date-csv-files\”}
Then its simply a matter of putting the powershell scripts together under one script like this
{.\dotnet.ps1
.\updates-ie11-Flashplayer.ps1
.\updates-office-2010.ps1
.\updates-office-2013.ps1
.\updates-office-2016.ps1
.\updates-rollup
.\updates-Windows-2008.ps1
.\updates-Windows-2016.ps1
.\updates-server-software.ps1
Start-Sleep -s 120
.\convert.ps1}
Hi
In order to include computers reporting to replica servers you need to add the line:
$CompSc.IncludeDownstreamComputerTargets = $true
below $CompSc declaration
it is also required to configure WSUS to roll up update and computer status from replica servers. Last but not least – don’t forget to sync WSUS replica status as otherwise report may not be valid and some computer/patch status may be missing.
Thanks for sharing this, it's the best WSUS report tool I've run into. For a compliance request I needed to prove a hotfix was installed. A small change to the report line saved me countless time.
hello,
would it be possible to get such report for all available KBs on WSUS server ?
Thanks !
Where is the script?
Is there are way to included updates that are listed as "needed" – current script doesn't seem to allow for this.
This is very helpful. Thank you so much. Stock Microsoft WSUS reporting will make you cry.
I am getting below error when running the script can you pls tell how to fix it
You cannot call a method on a null-valued expression.
Exception calling “GetUpdateServer” with “3” argument(s): “The remote name could not be resolved: ‘wsus'”