PowerShell

Adding Script to Your PowerShell HTML Reports

I have been having a lot of fun with this series of articles demonstrating a variety of tips, tricks, and techniques to generate killer HTML reports with PowerShell and ConvertTo-HTML. By all means, use as little or as much as you want. If you missed the previous articles, get caught up before continuing. I will not explain the previously covered material.

 

 

Sponsored Content

What is “Inside Microsoft Teams”?

“Inside Microsoft Teams” is a webcast series, now in Season 4 for IT pros hosted by Microsoft Product Manager, Stephen Rose. Stephen & his guests comprised of customers, partners, and real-world experts share best practices of planning, deploying, adopting, managing, and securing Teams. You can watch any episode at your convenience, find resources, blogs, reviews of accessories certified for Teams, bonus clips, and information regarding upcoming live broadcasts. Our next episode, “Polaris Inc., and Microsoft Teams- Reinventing how we work and play” will be airing on Oct. 28th from 10-11am PST.

I will confess right up front that I am not a web developer or even close. I know some basic HTML techniques and like you, have gleaned ideas from searching the Internet. One of those techniques is the use of javascript. While I probably could not write a javascript function from scratch, I do know how to use it in my PowerShell scripts. At some point in the past, I found some functions to create expandable sections. This is great for a long HTML document because you can collapse one or more sections.

In my header here string, where I define my CSS, I am going to insert this code.

<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js'>
</script>
<script type='text/javascript'>
function toggleDiv(divId) {
   `$("#"+divId).toggle();
}
function toggleAll() {
    var divs = document.getElementsByTagName('div');
    for (var i = 0; i < divs.length; i++) {
        var div = divs[i];
        `$("#"+div.id).toggle();
    }
}

The code assumes you will have a named DIV section that you want to collapse or expand. Let’s revise the example from the last article.


$computername = $env:COMPUTERNAME

$fragments = @()

#insert a graphic
$ImagePath = "c:\scripts\db.png"
$ImageBits =  [Convert]::ToBase64String((Get-Content $ImagePath -Encoding Byte))
$ImageFile = Get-Item $ImagePath
$ImageType = $ImageFile.Extension.Substring(1) #strip off the leading .
$ImageTag = "<Img src='data:image/$ImageType;base64,$($ImageBits)' Alt='$($ImageFile.Name)' style='float:left' width='120' height='120' hspace=10>"

$top = @"
<table>
<tr>
<td class='transparent'>$ImageTag</td><td class='transparent'><H1>System Report - $Computername</H1></td>
</tr>
</table>
"@

$fragments+=$top

Most of this should look familiar to you by now. One thing I am doing differently is putting the graphic in a table with the document title. I do not want this table to follow the same style as other tables. Therefore, I am defining a style called ‘transparent’ that sets the cell background color to the same as the document background color.
.transparent {
background-color:#E5E4E2;
}

Now, let’s add the new stuff. I want to define some section headings that when clicked, will toggle between expanded and collapsed. I also want to include a link at the beginning to toggle all sections. I will add the necessary script code to my array of fragments.
$fragments+="<a href='javascript:toggleAll();' title='Click to toggle all sections'>+/-</a>"

This will insert a link with the text “+/-“. When clicked, this will invoke the toggleAll javascript function. Now, we wll add the first section.
$Text = "Operating System"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

I am calling the DIV section the same as the text but I need to remove any spaces for the DIV id. After this section heading, I need to insert my content and add the closing DIV tag.
$fragments+= Get-Ciminstance -ClassName win32_operatingsystem -ComputerName $computername | 
Select @{Name="Operating System";Expression= {$_.Caption}},Version,InstallDate |
ConvertTo-Html -Fragment -As List
$fragments+="</div>"

I repeat the process as needed.
$Text = "System Information"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

$fragments+= Get-systeminfo -Computername $computername| ConvertTo-Html -Fragment -As List
$fragments+="</div>"

$Text = "Disk Information"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

#highlight low free space in red
[xml]$html = Get-DiskInfo -Computername $computername| ConvertTo-Html -Fragment
for ($i=1;$i -le $html.table.tr.count-1;$i++) {
  if ($html.table.tr[$i].td[-1] -le 20) {
    $class = $html.CreateAttribute("class")
    $class.value = 'alert'
    $html.table.tr[$i].childnodes[3].attributes.append($class) | out-null
  }
}

$fragments+= $html.InnerXml
$fragments+="</div>"

When finished, I can create my final document from the collection of fragments. For your convenience, here is a script with all of my final code.
[cmdletbinding()]
Param(
[string]$computername = $env:COMPUTERNAME,
#the path to the final htm report
[string]$Path = "d:\temp\systemreport.htm",
#set your own default graphic or delete default value
[string]$ImagePath = "c:\scripts\db.png"
)

#region helper functions

Function Get-SystemInfo {
[cmdletbinding()]
Param([string]$Computername = $env:COMPUTERNAME)

$cs = Get-CimInstance -ClassName Win32_computersystem #-ComputerName $Computername  
#this assumes a single processor
$proc = Get-CimInstance -ClassName win32_processor #-ComputerName $Computername 
 
$data = [ordered]@{
TotalPhysicalMemGB = $cs.TotalPhysicalMemory/1GB -as [int]
NumProcessors = $cs.NumberOfProcessors
NumLogicalProcessors = $cs.NumberOfLogicalProcessors
HyperVisorPresent = $cs.HypervisorPresent
DeviceID = $proc.DeviceID
Name = $proc.Name
MaxClock = $proc.MaxClockSpeed
L2size = $proc.L2CacheSize
L3Size = $proc.L3CacheSize

}

New-Object -TypeName PSObject -Property $data

}

Function Get-DiskInfo {
[cmdletbinding()]
Param([string]$Computername = $env:COMPUTERNAME)

Get-CimInstance -ClassName win32_logicaldisk -filter "drivetype=3" -ComputerName $Computername |
Select DeviceID,
@{Name="SizeGB";Expression = {$_.size/1gb -as [int]}},
@{Name="FreeGB";Expression={ [math]::round($_.Freespace/1gb,2)}},
@{Name="PctFree";Expression={[math]::round(($_.freespace/$_.size)*100,2)}}

}

#endregion

$fragments = @()

if (Test-Path $ImagePath) {
    #insert a graphic
    $ImageBits =  [Convert]::ToBase64String((Get-Content $ImagePath -Encoding Byte))
    $ImageFile = Get-Item $ImagePath
    $ImageType = $ImageFile.Extension.Substring(1) #strip off the leading .
    $ImageTag = "<Img src='data:image/$ImageType;base64,$($ImageBits)' Alt='$($ImageFile.Name)' style='float:left' width='120' height='120' hspace=10>"
}
else {
    Write-Warning "Could not find image file $ImagePath"
}
$top = @"
<table>
<tr>
<td class='transparent'>$ImageTag</td><td class='transparent'><H1>System Report - $Computername</H1></td>
</tr>
</table>
"@

$fragments+=$top

$fragments+="<a href='javascript:toggleAll();' title='Click to toggle all sections'>+/-</a>"

$Text = "Operating System"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

$fragments+= Get-Ciminstance -ClassName win32_operatingsystem -ComputerName $computername | 
Select @{Name="Operating System";Expression= {$_.Caption}},Version,InstallDate |
ConvertTo-Html -Fragment -As List
$fragments+="</div>"

$Text = "System Information"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

$fragments+= Get-systeminfo -Computername $computername| ConvertTo-Html -Fragment -As List
$fragments+="</div>"

$Text = "Disk Information"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

#highlight low free space in red
[xml]$html = Get-DiskInfo -Computername $computername| ConvertTo-Html -Fragment
for ($i=1;$i -le $html.table.tr.count-1;$i++) {
  if ($html.table.tr[$i].td[-1] -le 20) {
    $class = $html.CreateAttribute("class")
    $class.value = 'alert'
    $html.table.tr[$i].childnodes[3].attributes.append($class) | out-null
  }
}

$fragments+= $html.InnerXml
$fragments+="</div>"

$Text = "EventLog Info"
$div = $Text.Replace(" ","_")
$fragments+= "<a href='javascript:toggleDiv(""$div"");' title='click to collapse or expand this section'><h2>$Text</h2></a><div id=""$div"">"

[xml]$html  = Get-Eventlog -List -ComputerName $computername  | 
Select @{Name="Max(K)";Expression = {"{0:n0}" -f $_.MaximumKilobytes }},
@{Name="Retain";Expression = {$_.MinimumRetentionDays }},
OverFlowAction,@{Name="Entries";Expression = {"{0:n0}" -f $_.entries.count}},
@{Name="Log";Expression = {$_.LogDisplayname}} | ConvertTo-Html -Fragment

for ($i=1;$i -le $html.table.tr.count-1;$i++) {
  if ($html.table.tr[$i].td[3] -eq 0) {
    $class = $html.CreateAttribute("class")
    $class.value = 'alert'
    $html.table.tr[$i].attributes.append($class) | out-null
  }
}

$fragments+= $html.InnerXml
$fragments+="</div>"

$fragments+= "<p class='footer'>$(get-date)</p>"


$head = @"
<Title>System Report - $($env:computername)</Title>
<style>
body { background-color:#E5E4E2;
       font-family:Monospace;
       font-size:10pt; }
td, th { border:0px solid black; 
         border-collapse:collapse;
         white-space:pre; }
th { color:white;
     background-color:black; }
table, tr, td, th { padding: 2px; margin: 0px ;white-space:pre; }
tr:nth-child(odd) {background-color: lightgray}
table { width:95%;margin-left:5px; margin-bottom:20px;}
h2 {
 font-family:Tahoma;
 color:#6D7B8D;
}
.alert {
 color: red; 
 }
.footer 
{ color:green; 
  margin-left:10px; 
  font-family:Tahoma;
  font-size:8pt;
  font-style:italic;
}
.transparent {
background-color:#E5E4E2;
}

</style>
<script type='text/javascript' src='https://ajax.googleapis.com/ajax/libs/jquery/1.4.4/jquery.min.js'>
</script>
<script type='text/javascript'>
function toggleDiv(divId) {
   `$("#"+divId).toggle();
}
function toggleAll() {
    var divs = document.getElementsByTagName('div');
    for (var i = 0; i < divs.length; i++) {
        var div = divs[i];
        `$("#"+div.id).toggle();
    }
}
</script>

"@


$convertParams = @{ 
  head = $head 
  body = $fragments
}

convertto-html @convertParams | out-file -FilePath $Path

Get-Item -Path $Path

I have added my default values. You would want to change them for yourself. When I run the script, I end up with a file like this.

A collapsed HTML report (Image Credit: Jeff Hicks)
A Collapsed HTML Report (Image Credit: Jeff Hicks)

I have clicked the toggle all link to collapse all the sections. I can click in the individual section headings to toggle them.

Expanded sections (Image Credit: Jeff Hicks)
Expanded Sections (Image Credit: Jeff Hicks)

What do you think? Is this something you think you would find useful? This is definitely something you want to try for yourself and feel free to experiment. Make sure you try the CSS settings, assuming you are like me, and have limited experience with it.

I hope you found this series of articles enjoyable. If there is some other HTML-related element that you would like to add to your PowerShell reports, ping me on Twitter @jeffhicks.

Related Topics:

BECOME A PETRI MEMBER:

Don't have a login but want to join the conversation? Sign up for a Petri Account

Register
Comments (0)

Leave a Reply

External Sharing and Guest User Access in Microsoft 365 and Teams

This eBook will dive into policy considerations you need to make when creating and managing guest user access to your Teams network, as well as the different layers of guest access and the common challenges that accompany a more complicated Microsoft 365 infrastructure.

You will learn:

  • Who should be allowed to be invited as a guest?
  • What type of guests should be able to access files in SharePoint and OneDrive?
  • How should guests be offboarded?
  • How should you determine who has access to sensitive information in your environment?

Sponsored by:

 
Live Webinar: Active Directory Security: What Needs Immediate Priority!Live on Tuesday, October 12th at 1 PM ET

Attacks on Active Directory are at an all-time high. Companies that are not taking heed are being punished, both monetarily and with loss of production.

In this webinar, you will learn:

  • How to prioritize vulnerability management
  • What attackers are leveraging to breach organizations
  • Where Active Directory security needs immediate attention
  • Overall strategy to secure your environment and keep it secured

Sponsored by: