Get-WSUSReport

<#
.NOTES
    Name: Get-WSUSReport
    Author: Scott Babcock
.SYNOPSIS
    Gets WSUS information for a specific group and genreates an HTML email report.
.DESCRIPTION
    The script Get-WSUSReport loads the WSUS assembly and makes a connection to 
    the WSUS server in your environment. It targets a specific group and checks
    for the status of updates on each server within the group.  The HTML report
    lists a table of updates per server and a second table of servers per update.
.EXAMPLE  
    [PS] C:\>.\Get-WsusReport.ps1 <no parameters>
    The script does not have any parameters.
#>

# Create empty arrays.
$UpdateStatus = @()
$SummaryStatus = @()
$ServersPerUpdate = @()

# Load WSUS assembly.
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
# Connect to WSUS server and set the connection object into a variable.
$WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer("wsus.get-mailbox.net", $true, 8531)

# Record the last time that WSUS was syncronized with updates.
$LastSync = ($wsus.GetSubscription()).LastSynchronizationTime

# Create a default update scope object.
$UpdateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
# Modify the update scope ApprovedStates value from "Any" to "LatesRevisionApproved".
$UpdateScope.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
# Create a computerscope object for use as an a requred part of a method below.
$ComputerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope

# Get the "Servers" WSUS group using a hardcoded ID value.
$ComputerTargetGroups = $WSUS.GetComputerTargetGroups() `
| Where { $_.id -eq 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx' }
# Get all the computers objects that are members of the "Servers" group and set into variable
$MemberOfGroup = $wsus.getComputerTargetGroup($ComputerTargetGroups.Id).GetComputerTargets()

# Use a foreach loop to process summaries per computer for each member of the "Servers" group. `
#  Then populate an array with a updates needed.
Foreach ($Object in $wsus.GetSummariesPerComputerTarget($updatescope, $computerscope)) {
	# Use a nested foreach to process the CES Mail Servers members.	
	foreach ($object1 in $MemberOfGroup) {
		# Use an if statement to match the wsus objects that contain update summaries with
		#  the members of the CES Mail servers members.
		If ($object.computertargetid -match $object1.id) {
			# Set the fulldomain name of the CES Mail Server member in a variable.
			$ComputerTargetToUpdate = $wsus.GetComputerTargetByName($object1.FullDomainName)
			# Filter the server for updates that are marked for install with the state
			#  being either downloaded or notinstalled.  These are updates that are needed.
			$NeededUpdate = $ComputerTargetToUpdate.GetUpdateInstallationInfoPerUpdate() `
			| where {
				($_.UpdateApprovalAction -eq "install") -and `
				(($_.UpdateInstallationState -eq "downloaded") -or `
				($_.UpdateInstallationState -eq "notinstalled"))
			}
			
			# Null out the following variables so that they don't contaminate
			#  op_addition variables in the below nested foreach loop.
			$FailedUpdateReport = $null
			$NeededUpdateReport = $null
			# Use a nested foreach loop to accumulate and convert the needed updates to the KB number with URL in
			# an HTML format.
			if ($NeededUpdate -ne $null) {
				foreach ($Update in $NeededUpdate) {
					$myObject2 = New-Object -TypeName PSObject
					$myObject2 | add-member -type Noteproperty -Name Server -Value (($object1 | select -ExpandProperty FullDomainName) -replace ".FQDN", "")
					$myObject2 | add-member -type Noteproperty -Name Update -Value ('<a href' + '=' + '"' + ($wsus.GetUpdate([Guid]$update.updateid).AdditionalInformationUrls) + '"' + '>' + (($wsus.GetUpdate([Guid]$update.updateid)).title) + '<' + '/' + 'a' + '>')
					$UpdateStatus += $myObject2
					
					if ($Update.UpdateInstallationState -eq "Failed") {
						$FailedUpdateReport += ('<a href' + '=' + '"' + ($wsus.GetUpdate([Guid]$update.updateid).AdditionalInformationUrls) `
						+ '"' + '>' + "(" + (($wsus.GetUpdate([Guid]$update.updateid)).KnowledgebaseArticles) + ") " + '<' + '/' + 'a' + '>')
					}
					if ($Update.UpdateInstallationState -eq "Notinstalled" -or $Update.UpdateInstallationState -eq "Downloaded") {
						$NeededUpdateReport += ('<a href' + '=' + '"' + ($wsus.GetUpdate([Guid]$update.updateid).AdditionalInformationUrls) `
						+ '"' + '>' + "(" + (($wsus.GetUpdate([Guid]$update.updateid)).KnowledgebaseArticles) + ") " + '<' + '/' + 'a' + '>')
					}
				}
			}
			# Create a custom PSObject to contain summary data about each server and updates needed.
			$myObject1 = New-Object -TypeName PSObject
			$myObject1 | add-member -type Noteproperty -Name Server -Value (($object1 | select -ExpandProperty FullDomainName) -replace ".FQDN", "")
			$myObject1 | add-member -type Noteproperty -Name UnkownCount -Value $object.UnknownCount
			$myObject1 | add-member -type Noteproperty -Name NotInstalledCount -Value $object.NotInstalledCount
			$myObject1 | add-member -type Noteproperty -Name NotApplicable -Value $object.NotApplicableCount
			$myObject1 | add-member -type Noteproperty -Name DownloadedCount -Value $object.DownloadedCount
			$myObject1 | add-member -type Noteproperty -Name InstalledCount -Value $object.InstalledCount
			$myObject1 | add-member -type Noteproperty -Name InstalledPendingRebootCount -Value $object.InstalledPendingRebootCount
			$myObject1 | add-member -type Noteproperty -Name FailedCount -Value $object.FailedCount
			$myObject1 | add-member -type Noteproperty -Name ComputerTargetId -Value $object.ComputerTargetId
			$myObject1 | add-member -type Noteproperty -Name NeededCount -Value ($NeededUpdate | measure).count
			$myObject1 | add-member -type Noteproperty -Name Failed -Value $FailedUpdateReport
			$myObject1 | add-member -type Noteproperty -Name Needed -Value $NeededUpdateReport
			$SummaryStatus += $myObject1
		}
	}
}
$uniqueupdates = $UpdateStatus | sort -Unique update | select update

foreach ($uniqueupdate in $uniqueupdates) {
	$servers = $null
	$myObject3 = New-Object -TypeName PSObject
	$myObject3 | add-member -type Noteproperty -Name Update -Value $uniqueupdate.update
	foreach ($object in $UpdateStatus) {
		if ($object.Update -eq $uniqueupdate.update) {
			$servers += $object.server + " "
		}
	}
	$myObject3 | add-member -type Noteproperty -Name Servers -Value $servers
	$ServersPerUpdate += $myObject3
}

# Rewrite the array and eliminate servers that have 0 for needed updates.
$SummaryStatus = $SummaryStatus | where { $_.neededcount -ne 0 } | sort server

# List a summary of changes in a special table leveraging the "First" table class style listed above.
$WSUSHead += "<table class=`"First`">`r`n"
# Note the LastSync time.
$WSUSHead += "<tr><td class=`"First`"><b>Last Sync:</b></td><td class=`"First`"> " + `
$LastSync + "</td></tr>`r`n"
$WSUSHead += "</Body>`r`n"
$WSUSHead += "</Style>`r`n"
$WSUSHead += "</Head>`r`n"

# Create a generic HTML Header to use throughout the script for the body of the `
#  email message with table styles to control the formatting of any tables present it it.
$HTMLHead = "<Html xmlns=`"http://www.w3.org/1999/xhtml`">`r`n"
$HTMLHead += "<Head>`r`n"
$HTMLHead += "<Style>`r`n"
$HTMLHead += "TABLE{border: 1px solid black; border-collapse: collapse; font-family: Arial, Helvetica, sans-serif; font-size: 8pt;}`r`n"
$HTMLHead += "TH{border: 1px solid black; background: #dddddd; padding: 5px; color: #000000;}`r`n"
$HTMLHead += "TD{border: 1px solid black; padding: 5px;}`r`n"
$HTMLHead += "TABLE.First{border:1px solid #dddddd; background: #f6f6f6;}`r`n"
$HTMLHead += "TD.First{border:1px solid #dddddd; font-family: Arial, Helvetica, sans-serif; font-size: 8pt;}`r`n"
$HTMLHead += "H3{text-align:left; font-family: Arial, Helvetica, sans-serif; font-size: 15pt;}`r`n"
$HTMLHead += "HR{border: 2px dashed #848484;}`r`n"
$HTMLHead += "</Style>`r`n"
$HTMLHead += "</Head>`r`n"

# Build a variable with HTML for sending a report.
$UpdatesHTML = $HTMLHead
# Continue building HTML with the updates needed
$UpdatesHTML += $SummaryStatus | convertto-html -Fragment `
@{ Label = "Server"; Expression = { $_.server } }, @{ Label = "Needed Count"; Expression = { $_.NeededCount } }, @{ Label = "Not Installed"; Expression = { $_.NotInstalledCount } }, `
@{ Label = "Downloaded"; Expression = { $_.DownloadedCount } }, @{ Label = "Pending Reboot"; Expression = { $_.InstalledPendingRebootCount } }, @{ Label = "Failed Updates"; Expression = { $_.FailedCount } }, `
@{ Label = "Needed"; Expression = { $_.Needed } }

$ServersHTML = $ServersPerUpdate | convertto-html -Fragment `
@{ Label = "Update"; Expression = { $_.update } }, @{ Label = "Servers"; Expression = { $_.servers } }

# Add an assembly to fix up powershell HTML markup. Ensures all special characters
# are converted correctly.
Add-Type -AssemblyName System.Web
$UpdatesHTML = [System.Web.HttpUtility]::HtmlDecode($UpdatesHTML)
$ServersHTML = [System.Web.HttpUtility]::HtmlDecode($ServersHTML)

# Create HTML email by adding all the various HTML sections from above.
$MailMessage = "
<html>
 <body>
  $WSUSHead
  $UpdatesHTML
   <br>
  $ServersHTML
 </body>
</html>
"

# Get the date and time.
$DateTime = Get-Date -Format "ddd MM/dd/yyyy h:mm tt"
# Set subject line to include the $DateTime variable.
$EmailSubject = "Update Status for " + $DateTime

# Send an email with all the compiled data.
[string[]]$EmailTo = "babcockscott@Get-Mailbox.net"
Send-MailMessage -To $EmailTo `
-Subject $EmailSubject -From "donotreply@Get-Mailbox.net" `
-Body $MailMessage -BodyasHTML `
-SmtpServer "MailRelayServer"

 

17 thoughts on “Get-WSUSReport”

  1. This script is indeed a very nice piece of art!!

    Thank you very much for sharing, I have it already running in our production systems ;)

    Best regards,

    Reply
  2. Great script, i was hoping someone could help me how i could add a filter to only include updates under the “Windows” classification. i.e Server OS only, no office, SQL etc etc.?

    Reply
  3. wow .. what a script !!!!!!! excellent work!!!

    i did add some code to get a local copy of that report and send it as attachment (starting at line 173):

    $MailMessage | Out-File c:\temp\wsusreport.html
    # Get the date and time.
    $DateTime = Get-Date -Format “ddd MM/dd/yyyy h:mm tt”
    # Set subject line to include the $DateTime variable.
    $EmailSubject = “your subject ” + $DateTime

    # Send an email with all the compiled data.
    [string[]]$EmailTo = “yourmail@anymail.com”
    Send-MailMessage -To $EmailTo `
    -Subject $EmailSubject -From “yourmail@anymail.com” `
    -Body $MailMessage -BodyasHTML `
    -attachment “c:\temp\wsusreport.html” `
    -SmtpServer “yourmailgateway”

    Reply
  4. Fantastic script! This is EXACTLY what I was hoping to do, but much nicer than how I was going to do it. What I’d like to do with it is save the htmls of all the computer groups (which the script already does) to a folder within inetpub\wwwroot and would like to know if there’s possibly a way to have it generate an index.html with links to the html files it created.

    Reply
  5. I ma getting below error while running the script.

    Cannot find an overload for “getComputerTargetGroup” and the argument count: “1”.
    At Z:\WSUS\WSUS-Report.ps1:41 char:1
    + $MemberOfGroup = $wsus.getComputerTargetGroup($ComputerTargetGroups.Id).GetCompu …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFindBest

    Reply
  6. Exception calling “GetUpdateServer” with “3” argument(s): “The underlying connection was closed: An unexpected error occurred on a
    send.”
    At line:25 char:1
    + $WSUS = [Microsoft.UpdateServices.Administration.AdminProxy]::getUpdateServer(“d …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodInvocationException
    + FullyQualifiedErrorId : WebException

    Cannot find an overload for “getComputerTargetGroup” and the argument count: “1”.
    At line:41 char:1
    + $MemberOfGroup = $wsus.getComputerTargetGroup($ComputerTargetGroups.Id).GetCompu …
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : NotSpecified: (:) [], MethodException
    + FullyQualifiedErrorId : MethodCountCouldNotFindBest

    Send-MailMessage : The remote name could not be resolved: ‘MailRelayServer’
    At line:181 char:1
    + Send-MailMessage -To $EmailTo `
    + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo : InvalidOperation: (System.Net.Mail.SmtpClient:SmtpClient) [Send-MailMessage], SmtpException
    + FullyQualifiedErrorId : SmtpException,Microsoft.PowerShell.Commands.SendMailMessage

    Reply
  7. At line:9 char:8
    + for the status of updates on each server within the group. The H …
    + ~
    Missing opening ‘(‘ after keyword ‘for’.
    At line:12 char:10
    + [PS] C:\>.\Get-WsusReport.ps1
    + ~~~~~~~~~~~~~~~~~~~~~~~~
    Unexpected token ‘C:\>.\Get-WsusReport.ps1’ in expression or statement.
    At line:12 char:35
    + [PS] C:\>.\Get-WsusReport.ps1
    + ~
    The ‘<' operator is reserved for future use.
    + CategoryInfo : ParserError: (:) [], ParentContainsErrorRecordException
    + FullyQualifiedErrorId : MissingOpenParenthesisAfterKeyword

    Reply
  8. I found I was getting all the KB article links showing as one big underline, although the individual links worked if you hovered your mouse above the KB number. I added a &nbsp to separate them in the $Update.UpdateInstallationState -eq “Notinstalled” and $Update.UpdateInstallationState -eq “Failed” sections of code

    Reply
  9. I added an “$_.UpdateInstallationState -eq “failed”” as another “or” at line 59. Wasn’t getting any failed links.

    Reply

Leave a Comment