Last Update: Sep 04, 2024 | Published: Jun 20, 2016
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:workreport.htm
Don’t forget you still need to save the results to a file.
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:scriptsblue.css | out-file C:workreport.htm
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:workHotFixReport.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:scriptscomputers.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:
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.
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:workHotFixReport.htm" ) #import computer information $computers = Import-Csv C:scriptscomputers.csv #dot source the hot fix function . C:scriptsAdvancedFunction-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("<","<").Replace(">",">" ) } #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.