Creating Simple PowerShell HotFix HTML Reports

powershell hero
Recently I went through a series of articles building a hot fix reporting tool. If I recall correctly, part of the original forum post was about creating reports and I think that meant HTML reports. Well, even if it didn’t, IT pros (or maybe their managers) love HTML reports. PowerShell makes this a relatively simple task with ConvertTo-HTML.  With this cmdlet and the hot fix function, it is pretty easy to create an html report.

Get-MyHotFix -Computername chi-p50,chi-hvr2 -After 5/1/2016 | ConvertTo-html -Title "HotFix Report" | out-file C:\work\report.htm

Don’t forget you still need to save the results to a file.

bare HTML hotfix report
bare HTML hotfix report (Image Credit: Jeff Hicks)

The report is pretty bare bones. But if you look at help for ConvertTo-HTML, you’ll see that you can specify a path to a CSS file.

Get-MyHotFix -Computername chi-p50,chi-hvr2 -After 5/1/2016 | ConvertTo-html -Title "HotFix Report" -CssUri C:\scripts\blue.css | out-file C:\work\report.htm

a more colorful HTML report
a more colorful HTML report (Image Credit: Jeff Hicks)

If you are saving the file to a network share or to an intranet server, as along as the CSS file is available to everyone this works pretty well. But I wouldn’t want to have to type that long command everytime I needed a report. Or, suppose this was a monthly task and I was out of the office. I don’t want to have to rely on someone else to type the command correctly. This is where a PowerShell script is useful. All I need to do is copy and paste the command into a .ps1 file and run it. But since I’m going to go the effort of creating a script file, I might as well create something meaningful.
Let’s say that every month I need to run a report that shows all hot fixes installed in the last 45 days on all my servers.  The 45 day mark is arbitrary and I want to allow the option to search for a different number of days to satisfy the boss’ whims. I also know the file name will always be the same, but there may be times I need a different path. I can create a script with parameters to meet these requirements.

Param(
[int]$Days = 45,
$Path = "C:\work\HotFixReport.htm"
)

The script will always process the same computers pulled from a CSV file, so that will be the first line of my script.

$computers = import-csv C:\scripts\computers.csv

I probably should have some error handling, and this could also have been a parameter, but for the sake of demonstration will go with this.
</H2>" $fragments+= $item.Group | Select-Object -Property * -ExcludeProperty Computername | ConvertTo-HTML -Fragment }

The $fragments variable is now the collection of HTML code. Personally, I like to embed the CSS style into the document to make it portable, so my script is going to define a head section as well as a footer with the report date.

#html report title
$ReportTitle = "Company Hotfix Report - $Days Days"
#define a header with an embedded style sheet
$head = @"
body { background-color:#d5dbdb;
       font-family:tahoma;
       font-size:10pt; }
td, th { border:1px solid black;
         border-collapse:collapse; }
th { color:white;
     background-color:black; }
table, tr, td, th { padding: 2px; margin: 0px }
table { width:95%;margin-left:5px; margin-bottom:20px;}
</style>
<br>
<H1>$ReportTitle</H1>
"@
$footer = "<H5><i>Report run $(Get-Date)</i></H5>"

All that remains is to create the HTML report.

ConvertTo-HTML -Head $head -body $fragments -PostContent $footer | Out-File -FilePath $Path -Encoding ascii

Here’s the final result:

A grouped HTML report
A grouped HTML report (Image Credit: Jeff Hicks)

So I show the finished report to the boss who gives me a new requirement that Security Updates should be highlighted. I also realize that the online link is a url. So wouldn’t it be nice to be able to click on it to learn more about a given hotfix? These changes will require me to modify the HTML code on the fly. There’s nothing in the Convertto-HTML cmdlet that will help.
To highlight the security update, I can change the font color to red is the description matches ‘Security Update’. I can do this by inserting a class with a corresponding style entry.

.security { color:red;}

Modifying the HTML to detect and insert is a bit trickier. The trick I use is to save the fragment as XML.

[xml]$frag = $item.Group | Select-Object -Property * -ExcludeProperty Computername |ConvertTo-HTML -Fragment -as Table

With an XML document, it is easier to find things using an XPath query and then modify the matching nodes. In my case, that means inserting the class attribute.

$frag.SelectNodes("//td[text()='Security Update']") | foreach {
      $class = $frag.CreateAttribute("class")
      $class.value = 'security'
      $_.Attributes.append($class) | Out-Null
    }

The XPath query is looking for td elements with a text value of ‘Security Update’. I can do something similar with the online url.

$frag.SelectNodes("//*[contains(text(),'http')]") | foreach {
       #get the current value
      $url = $_.'#text'
      #replace the value with html link
      $_.'#text' =  "<a href=$url target=_blank>$url</a>"
    }

In this case I am replacing the text with an html link. The last step for this trick to work is to replace some characters in the InnerXML property.

$fragments+= $frag.InnerXml.replace("<","<").Replace(">",">" )

The $Fragments variable is now HTML code and the rest of the script is the same, but with better results.

html hotfix report with highlights and links
html hotfix report with highlights and links (Image Credit: Jeff Hicks)

That seems pretty snazzy to me! Here’s the complete script:

#requires -version 4.0
#create an HTML hotfix report
Param(
[int]$Days = 45,
$Path = "C:\work\HotFixReport.htm"
)
#import computer information
$computers = Import-Csv C:\scripts\computers.csv
#dot source the hot fix function
. C:\scripts\AdvancedFunction-HotfixReport.ps1
#get all hotfixes installed since $Days days ago
Write-Host "Getting hot fix data...please wait" -foregroundcolor magenta
#group data by computername
$data = $computers | Get-MyHotFix -After (Get-Date).AddDays(-$days) | Group-Object -Property Computername
Write-Host "Preparing report..." -foregroundcolor magenta
#initialize an empty array
$fragments=@()
#create a fragments for each computername
foreach ($item in $data) {
    #define a heading with the computer name and total number of hotfixes
    $fragments+="<H2>$($item.name) [$($item.count)]</H2>"
    #convert data to an XML fragment
    [xml]$frag = $item.Group | Select-Object -Property * -ExcludeProperty Computername |
    ConvertTo-HTML -Fragment -as Table
    #insert security class for Security Updates
    $frag.SelectNodes("//td[text()='Security Update']") | foreach {
      $class = $frag.CreateAttribute("class")
      $class.value = 'security'
      $_.Attributes.append($class) | Out-Null
    }
    #turn urls into links. This assumes the entire text value is a url
    $frag.SelectNodes("//*[contains(text(),'http')]") | foreach {
       #get the current value
      $url = $_.'#text'
      #replace the value with html link
      $_.'#text' = "<a href=$url target=_blank>$url</a>"
    }
    #replace XML characters for <> in the body
    $fragments+= $frag.InnerXml.replace("&lt;","<").Replace("&gt;",">" )
}
#html report title
$ReportTitle = "Company Hotfix Report - $Days Days"
#define a header with an embedded style sheet
$head = @"
<Title>$ReportTitle</Title>
<style>
body { background-color:#D5DBDB;
       font-family:Tahoma;
       font-size:10pt; }
td, th { border:1px solid black;
         border-collapse:collapse; }
th { color:white;
     background-color:black; }
table, tr, td, th { padding: 2px; margin: 0px }
table { width:95%;margin-left:5px; margin-bottom:20px;}
.security { color:red;}
</style>
<br>
<H1>$ReportTitle</H1>
"@
$footer = "<H5><i>Report run $(Get-Date)</i></H5>"
#create the HTML report and save to a file
ConvertTo-HTML -Head $head -body $fragments -PostContent $footer | Out-File -FilePath $Path -Encoding ascii
#display the file object
Get-item -Path $path


But you know, one thing that I find annoying with this report, and hot fixes in general, is that I don’t know what problem the hot fix solving. If I click an online link I can read an article. What would be really handy would be to display the article title in the report. Wouldn’t that be useful? But it is tricky and not something I want to start now so check back later and we’ll wrap this up with an advanced HTML hotfix report.