Description

This script produces a report that shows server names, uptime, last date and time patched, and how many days have elapsed since the last patch. The report is then emailed.
The “Days Since Last Patch” column has the background color coded by severity as determined by number of days since the last patch that you set. For example, the screenshot below shows a Green background if the server has been patched in the last 30 days, Yellow means it has been patched between 30 - 90 days ago, and Red means it has not been patched in the last 90 days.
Edit the $ServerList path and file for your server names, one per line.
The $Warning and $Critical variables define the color coded background for the “Days Since Last Patched” column.
Also edit the email addresses, SMTP server name, and email subject if desired.
Added on 11/6/2019 is the option to have a .csv file attached to the email. Setting the value $IncludeCSV to “True” will activate this option. Set to “False” or any other value to not have it attached.

Thanks to Scot Johnson (Spicehead ScoJo) for the suggestion.

12/06/2019 RMW Modified error checking routine
- Thanks Scot Johnson (Spicehead ScoJo) for bug checking and reporting!

Source Code

<#===========================================================================================================================
 Script Name: ServerPatchReport.ps1
 Description: Reports uptime and the last date a patch was installed on servers.
      Inputs: List of server names fed from text file, one server name per line.
     Outputs: Server name, uptime, date and time the patch was installed, and how many days ago the server was patched.
       Notes: Added the option to have a .CSV file attached to the email.
     Example: .\ServerPatchReport.ps1
      Author: Richard Wright
Date Created: 11/3/2017
     Credits: https://damn.technology/get-latest-installed-update-powershell
              Scot Johnson (Spicehead: ScoJo) for bug reporting.
   ChangeLog: MM/DD/YYYY  Who  Description of changes
              ----------  ---  ----------------------------------------------------------------------------------------------
              11/04/2017  RMW  Added BG colors to highlight good, warning, critical days.
              11/06/2017  RMW  Added email notification.
              12/06/2017  RMW  Changed the querying processes
              12/20/2017  RWM  Aesthetics
              10/09/2018  RMW  Aesthetics
              11/06/2019  RMW  Added .CSV file option
              12/06/2019  RMW  Modified error checking routine
              07/06/2020  RMW  Added Uptime
              02/07/2022  RMW  Edited subject, minor changes for clarity

=============================================================================================================================
Variable List - in the section following this, edit these variables with your preferences:
   VARIABLE NAME     DESCRIPTION
   ----------------  --------------------------------------------------------------------------------------------------------
   $DateStamp        Format of dates shown in the report.
   $DateStampCSV     Format of dates shown in the CSV report - commas removed.
   $ServerList       File with the list of servernames for which to provide patch statistics; one per line.
   $ReportFileName   The outputted HTML filename and location
   $ReportFileCSV    The outputted CSV filename and location
   $IncludeCSV       If "True" the $ReportFileCSV will be emailed as an attachment
   $ReportTitle      Name of the report that is shown in the generated HTML file and in email subject.
   $EmailTo          Who should receive the report via email
   $EmailCc          Who should receive the report via email Cc:
   $EmailFrom        Sender email address
   $EmailSubject     Subject for the email
   $SMTPServer       SMTP server name
   $BGColorTbl       Background color for tables.
   $BGColorGood      Background color for "Good" results. #4CBB17 is a shade of green.
   $BGColorWarn      Background color for "Warning" results. #FFFC33 is a shade of yellow.
   $BGColorCrit      Background color for "Critical" results. #FF0000 is red.
   $Warning          # of days since last update to indicate Warning (Yellow) in report. Must be less than $Critical amount.
   $Critical         # of days since last update to indicate Critical (RED) in report. Must be more than $Warning amount.    
=============================================================================================================================#>
$ComputerName = $($env:COMPUTERNAME)
$ScriptPath = Get-Location

<#==============================
Edit these with your preferences
==============================#>
$DateStamp = (Get-Date -Format D)
$DateStampCSV = (Get-Date -Format "MMM-dd-yyyy")
$FileDateStamp = Get-Date -Format yyyyMMdd
$ServerList = Get-Content "$ScriptPath\ServerList.txt"
$ReportFileName = "$ScriptPath\ServerPatchReport-$FileDateStamp.html"
$ReportFileCSV = "$ScriptPath\ServerPatchReport.csv"
$IncludeCSV = "True"
$ReportTitle = "Server Patch Report"
$EmailTo = "to@domain.com"
$EmailFrom = "noreply@domain.com"
$EmailSubject = "From $ComputerName - Server Patch Report for $DateStamp"
$SMTPServer = "smtp.domain.com"
$BGColorTbl = "#EAECEE"
$BGColorGood = "#4CBB17"
$BGColorWarn = "#FFFC33"
$BGColorCrit = "#FF0000"
$Warning = 30
$Critical = 60


<#==================================================
Do not edit below this section
==================================================#>
Clear

<#==================================================
Begin MAIN
==================================================#>
# Create output file and nullify display output
New-Item -ItemType file $ReportFileName -Force > $null
New-Item -ItemType file $ReportFileCSV -Force > $null

<#==================================================
Write the HTML Header to the report files
==================================================#>
Add-Content $ReportFileName "<html>"
Add-Content $ReportFileName "<head>"
Add-Content $ReportFileName "<meta http-equiv='Content-Type' content='text/html; charset=iso-8859-1'>"
Add-Content $ReportFileName "<title>$ReportTitle</title>"
Add-Content $ReportFileName '<STYLE TYPE="text/css">'
Add-Content $ReportFileName "td {"
Add-Content $ReportFileName "font-family: Cambria;"
Add-Content $ReportFileName "font-size: 11px;"
Add-Content $ReportFileName "border-top: 1px solid #999999;"
Add-Content $ReportFileName "border-right: 1px solid #999999;"
Add-Content $ReportFileName "border-bottom: 1px solid #999999;"
Add-Content $ReportFileName "border-left: 1px solid #999999;"
Add-Content $ReportFileName "padding-top: 0px;"
Add-Content $ReportFileName "padding-right: 0px;"
Add-Content $ReportFileName "padding-bottom: 0px;"
Add-Content $ReportFileName "padding-left: 0px;"
Add-Content $ReportFileName "}"
Add-Content $ReportFileName "body {"
Add-Content $ReportFileName "margin-left: 5px;"
Add-Content $ReportFileName "margin-top: 5px;"
Add-Content $ReportFileName "margin-right: 5px;"
Add-Content $ReportFileName "margin-bottom: 10px;"
Add-Content $ReportFileName "table {"
Add-Content $ReportFileName "border: thin solid #000000;"
Add-Content $ReportFileName "}"
Add-Content $ReportFileName "</style>"
Add-Content $ReportFileName "</head>"
Add-Content $ReportFileName "<body>"
Add-Content $ReportFileName "<table width='75%' align=`"center`">"
Add-Content $ReportFileName "<tr bgcolor=$BGColorTbl>"
Add-Content $ReportFileName "<td colspan='4' height='25' align='center'>"
Add-Content $ReportFileName "<font face='Cambria' color='#003399' size='4'><strong>$ReportTitle<br></strong></font>"
Add-Content $ReportFileName "<font face='Cambria' color='#003399' size='2'>$DateStamp</font><br><br>"

#Add to CSV file
Add-Content $ReportFileCSV "$ReportTitle"
Add-Content $ReportFileCSV "$DateStampCSV`n"

# Add color descriptions
$Warn=$Warning+1
Add-content $ReportFileName "<table width='75%' align=`"center`">"  
Add-Content $ReportFileName "<tr>"  
Add-Content $ReportFileName "<td width='30%' bgcolor=$BGColorGood align='center'><strong>Patched <= $Warning Days</strong></td>"  
Add-Content $ReportFileName "<td width='30%' bgcolor=$BGColorWarn align='center'><strong>Patched $Warn - $Critical Days</strong></td>"  
Add-Content $ReportFileName "<td width='30%' bgcolor=$BGColorCrit align='center'><strong>Patched > $Critical Days</strong></td>"
Add-Content $ReportFileName "</tr>"
Add-Content $ReportFileName "</table>"

# Add Column Headers
Add-Content $ReportFileName "</td>"
Add-Content $ReportFileName "</tr>"
Add-Content $ReportFileName "<tr bgcolor=$BGColorTbl>"
Add-Content $ReportFileName "<td width='20%' align='center'><strong>Server Name</strong></td>"
Add-Content $ReportFileName "<td width='20%' align='center'><strong>Uptime</strong></td>"
Add-Content $ReportFileName "<td width='20%' align='center'><strong>Last Patch Date & Time</strong></td>"
Add-Content $ReportFileName "<td width='20%' align='center'><strong>Days Since Last Patch</strong></td>"
Add-Content $ReportFileName "</tr>"

#Add column headers to CSV file
Add-Content $ReportFileCSV "Server Name, Uptime, Last Patch Date & Time, Days Since Last Patch"

<#==================================================
Function to write the HTML footer
==================================================#>
Function writeHtmlFooter
{
	param($FileName)
	Add-Content $FileName "</table>"
	Add-content $FileName "<table width='75%' align=`"center`">"  
	Add-Content $FileName "<tr bgcolor=$BGColorTbl>"  
	Add-Content $FileName "<td width='75%' align='center'><strong>Total Servers: $ServerCount</strong></td>"
	Add-Content $FileName "</tr>"
	Add-Content $FileName "</table>"
	Add-Content $FileName "</body>"
	Add-Content $FileName "</html>"
	Add-Content $ReportFileCSV "`nEnd of Report"
}

<#==================================================
Function to write server update information to the
HTML report file
==================================================#>
Function writeUpdateData
{
	param($FileName,$Server,$Uptime,$InstalledOn)
	Add-Content $FileName "<tr>"
	Add-Content $FileName "<td align='center'>$Server</td>"
	Add-Content $FileName "<td align='center'>$Uptime</td>"
	Add-Content $FileName "<td align='center'>$InstalledOn</td>"
# Color BG depending on $Warning and $Critical days set in script
    If ($InstalledOn -eq "Error collecting data") 
    { 
        $DaySpanDays = "Error"
        $Uptime = "Error"
    }
    Else
    {
        $System = (Get-Date -Format "MM/dd/yyyy hh:mm:ss")
        $DaySpan = New-TimeSpan -Start $InstalledOn -End $System
        $DaySpanDays = $DaySpan.Days
    }
	If ($InstalledOn -eq "Error collecting data" -or $DaySpan.Days -gt $Critical)
	{
    	# Red for Critical or Error retrieving data
		Add-Content $FileName "<td bgcolor=$BGColorCrit align='center'>$DaySpanDays</td>"
		Add-Content $ReportFileCSV "$Server,$Uptime,$InstalledOn,$DaySpanDays"
	}
	ElseIf ($DaySpan.Days -le $Warning)
	{
	    # Green for Good
		Add-Content $FileName "<td bgcolor=$BGColorGood align=center>$DaySpanDays</td>"
		Add-Content $ReportFileCSV "$Server,$Uptime,$InstalledOn,$DaySpanDays"
	}
	Else
	{
	    # Yellow for Warning
		Add-Content $FileName "<td bgcolor=$BGColorWarn align=center>$DaySpanDays</td>"
		Add-Content $ReportFileCSV "$Server,$Uptime,$InstalledOn,$DaySpanDays"
	}

	 Add-Content $FileName "</tr>"
}

<#==================================================
Query servers for their update history
Try registry first, if error Get-Hotfix
==================================================#>
Write-Host "Querying servers for installed updates...`n" -foreground "Yellow"
$ServerCount = 0
ForEach ($Server in $ServerList)
{
        $InstalledOn = ""
	$BootTime = (Get-WmiObject win32_operatingSystem -computer $Server -ErrorAction stop).lastbootuptime
	$BootTime = [System.Management.ManagementDateTimeconverter]::ToDateTime($BootTime)
	$Now = Get-Date
	$Uptime = ""
	$span = New-TimeSpan $BootTime $Now 
		$Days = $span.days
		$Hours = $span.hours
		$Minutes = $span.minutes 

<#===============================
Remove plurals if the value = 1
=================================#>
	If ($Days -eq 1)
		{$Day = "1 day "}
	else
		{$Day = "$Days days "}

	If ($Hours -eq 1)
		{$Hr = "1 hr "}
	else
		{$Hr = "$Hours hrs "}

	If ($Minutes -eq 1)
		{$Min = "1 min"}
	else
		{$Min = "$Minutes mins"}

	$Uptime = $Day + $Hr + $Min

    Try
    {
        Write-host "Checking $Server..."
	$ServerCount++
        $key = "SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\Results\Install"
        $keytype = [Microsoft.Win32.RegistryHive]::LocalMachine 
        $RemoteBase = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($keytype,$Server)
        $regKey = $RemoteBase.OpenSubKey($key)
        $KeyValue = ""
        $KeyValue = $regkey.GetValue("LastSuccessTime")
        $InstalledOn = ""
        $InstalledOn = Get-Date $KeyValue -Format 'MM/dd/yyyy hh:mm:ss'

    }

    Catch 
    {
        $ServerLastUpdate = (Get-HotFix -ComputerName $Server | Sort-Object -Descending -Property InstalledOn -ErrorAction SilentlyContinue | Select-Object -First 1)
	$InstalledOn = $ServerLastUpdate.InstalledOn
    }

   If ($InstalledOn -eq "")
   {
	$InstalledOn = "Error collecting data"
	$Uptime = "Error collecting data"
   }

    writeUpdateData $ReportFileName $Server $Uptime $InstalledOn
}

Write-Host "Finishing report..." -ForegroundColor "Yellow"
writeHtmlFooter $ReportFileName

<#==================================================
Send email
==================================================#>
$BodyReport = Get-Content "$ReportFileName" -Raw

$SMTPsettings = @{
	To =  $EmailTo
	From = $EmailFrom
	Subject = $EmailSubject
	Body = $BodyReport
	SmtpServer = $SMTPServer
	}

$SMTPsettingsCSV = @{
	To =  $EmailTo
	From = $EmailFrom
	Subject = $EmailSubject
	Attachments = $ReportFileCSV
	Body = $BodyReport
	SmtpServer = $SMTPServer
	}

IF ($IncludeCSV -eq "True") {
	Send-MailMessage @SMTPsettingsCSV -BodyAsHtml
}ELSE {
	Send-MailMessage @SMTPsettings -BodyAsHtml
}

Screenshots

19 Spice ups

This script takes less than a minute to run in my environment with 85 Windows servers included in the report. I noticed that some Windows 2016 servers report midnight (00:00:00) as the time updates are installed but that is irrelevant to me as I am only concerned about the days and not the hours. Notice that Server2 says it was updated 39 days ago. That was because this report was ran before 11:33:17 on Dec 20 otherwise it would have shown 40 days. For your list of servers: use just the server name without the domain as it will look better in the report. Tested on server 2008, 2012, and 2016. Comments are appreciated and if you download and like it then please rate it.

Richard, Having a problem. I copied the SMTP settings from another daily report I have emailing out and used the same txt file with the servers but the report does bring back any server information back its blank and it does not send the email and just attempts to retry after 15 seconds. As i stated I have another script that is now scheduled task that runs the script once a day and then emails me the report and it is working. Using the same server list and email settings this script does not work. Please advise, Thank you!

Hi Chris, Sorry you’re having problems. I sent you a pm but now I can comment (don’t know why I couldn’t before) but anyway, check to see if the report was created. The line in the code that identifies it is here: $ReportFileName = “$ScriptPath\ServerPatchReport-$FileDateStamp.html” So you should be able to find the file “ServerPatchReport-DATE.html” where your script was run. Look to see if that file was created and let me know so we can troubleshoot the next step. Richard

Richard, The report is being created but still there is no data in the report besides the headers that are created when this script runs.

Try these: Run PowerShell as an administrator and try running the script from the command line to see the output. Make sure the serverlist file is correct: one server per line. Start with just 2 or 3 servers at first to see if the report is correct. I sent you a PM as well so feel free to call me to discuss. Richard

Richard thank you for the help with your script. This is one going to be one of my weekly task reports that run. Keep up the great work, and I look forward to see what you are going to write up next!

Great script, Richard! The only issue I had was related to regional settings on the machines in question. I think powershell is running as en-US but the OS environments are running en-EN so powershell can’t handle the date/time format. I tried to change the script format but it didn’t help. I fixed it through adding these 4 lines to the start of the script to change the region settings for that particular powershell thread; $culture = [System.Globalization.CultureInfo]::CreateSpecificCulture(“en-EN”) $culture.NumberFormat.NumberDecimalSeparator = “.” $culture.NumberFormat.NumberGroupSeparator = “,” [System.Threading.Thread]::CurrentThread.CurrentCulture = $culture The dates on the sheet are still showing up as MM/dd/yyyy but the script runs and produces accurate data so I’m not worried about that enough to fix it.

This script works perfectly and will definitely be a great addition to our scheduled tasks!!

changing-lives. I believe I’m getting a similar issue to what you had. I’m struggling to understand where you place the 4 lines you mentioned. Could you give an example of where you put the 4 lines that changes the region settings?

I’m not great with powershell but I got there in the end. Brilliant script, and thank you “changing-lives” for the additional 4 lines for us UK lot.

Thanks for the code Richard - very helpful. Is there a way to sort the output by date without a significant re-write of the code?

No, because it is an HTML report. However you can copy and paste the results into a spreadsheet and do it that way.

Right, I meant sorting by date prior to the HTML encoding.

Understood, but it would be a major rewrite. Not saying it can’t be done, just that it would take some time. I will research.

Thank you sir! I’m fiddling around with it as well, so I’ll let you know if I have any breakthroughs.

How can people in the UK get this script to run successfully? Can it modified so there’s a UK version?

Hi, could someone please post the script for UK users the date format as dd/MM/yyyy? Thanks

Five star script, thanks for the same. One question, basically its requirement " can i use this script to read servers directly from the active directory ? As of now i’m using through text file, Thanks in advance :).

Yes you can. Between these lines of code: $ScriptPath = Get-Location $ServerList = Get-Content “$ScriptPath\ServerList.txt” Add this: Get-ADComputer -Filter {(OperatingSystem -like “windowsserver*”) -and (Enabled -eq “True”)} -Properties * | SORT Name | Select -ExpandProperty Name | Outfile $ScriptPath\ServerListNames.txt That will read AD and will create/overwrite the “$ScriptPath\ServerList.txt” file with enabled servers where the OS contains Windows and Server. I use it to create the list I use.