Category Archives: PowerShell

QA Scripts – Version 4


Version 4 of my scripts have been released today, and with it a whole host of changes have been made.  Version 3 of the scripts are still available if you can’t move to v4 yet.

A recap

The QA checks came about as a need to verify the build of new servers for various customers and their different environments. All new server builds are usually done with a custom gold image template; however this image still lacks many of the additional tools and configuration settings needed before it can be accepted into support.


Most of this extra tooling and configuration should be automated, however checks are still needed to make sure each customer or environment has their specific settings in place.


Version 4

With version 4 came an almost total rewrite of the checks.  Every check script was looked at and changed to make sure it was using up-to-date code and more modern cmdlets.  The checks were also rewritten to remove the need to specify a specific server name in them, as they now run within a remote WinRM session.

You can grab them from here:



QA Engine

The QA Engine also got a minor update with tweaks and bug fixes.  The big change was the WinRM remoting.  This helps with making the checks faster and more reliable.


Settings Configuration Tool

The Settings Configuration Tool also got a make over.  with a search function for finding specific checks and a icons for the drop down selection boxes (not an easy feat).


HTML Report

There is also a new more modern report format.  The one I trialled with version 3 has been tweaked and tuned and is now the new default layout.  An example of which can be viewed here –


Change Log

A change log of major updates is listed here.  Of note the two major updates is that PowerShell v4 is now a requirement and that the entire package is now multilingual.  If people are willing to help me translate, the Checks, QA Engine and Settings Configuration Tool can now be shown in almost any language (Right-To-Left languages probably won’t work – I’ll be looking into this if there is a requirement).



The GitHub Wiki for this project has been updated with lots of documentation that should help you get up and running quickly in your environment.  The first guide you should look at is the Quick Start Guide.

There is also an in-depth guide to the Settings Configuration Tool.  This tool helps you to configure the scripts for your specific environments and is invaluable in the amount of time it saves.

OwnerDraw ComboBox


Drop down lists are great for selecting from a list of items.  However, a plain text list can be difficult to read if there are a lot of items.  Icons help to make a list easier to read, however the standard ComboBox control does not support images.


As with the ListView control that I customised here, the ComboBox can also be modified.

My new control supports not only adding icons, but also indenting and disabling specific items.


The first image shows multicoloured icons with randomly applied indenting.  Indents can be any value you require.  Only 0 and 1 are shown here.  The second image shows all green icons, however items 3 and 4 are “disabled”.  Notice that the icons and text appear greyed out.

The Code

As with the ListView control, a few more lines of code are required…

ComboBox Definition

# Define ComboBox control
$cmoComboBox                       = New-Object 'System.Windows.Forms.ComboBox'

# Add OwnerDraw code
$cmoComboBox.ItemHeight            = '22'
$cmoComboBox.DrawMode              = 'OwnerDrawFixed'

# Set ComboBox properties
$cmoComboBox.Location              = ' 12,  65'
$cmoComboBox.Size                  = '370,  35'
$cmoComboBox.Font                  = $sysFont
$cmoComboBox.DropDownStyle         = 'DropDownList'

# Add control to the main form

OwnerDraw Code

# Required Bits
[int]$script:ComboIcons_SelectedItem = 0               # Keeps track of the currently selected item
[System.Collections.ArrayList]$IconComboItems = @{}    # Holds all the custom items for the ComboxBox
Function New-IconComboItem { Return ( New-Object -TypeName PSObject -Property @{'Icon' = ''; 'Name' = ''; 'Text' = ''; 'Indent' = '0'; 'Enabled' = $True; } ) }

# Used to "disable" an image (turn it into a greyscale image of itself)
Function GreyScaleImage ([System.Drawing.Image]$Image)
    If ([string]::IsNullOrEmpty($Image) -eq $True) { Return $Image }
    [System.Drawing.Image]                  $newImage  = New-Object 'System.Drawing.Bitmap'($Image.Width, $Image.Height)
    [System.Drawing.Graphics]               $graphics  = [System.Drawing.Graphics]::FromImage($newImage)
    [System.Drawing.Imaging.ColorMatrix]    $matrix    = New-Object 'System.Drawing.Imaging.ColorMatrix'
    [System.Drawing.Imaging.ImageAttributes]$imgAttrib = New-Object 'System.Drawing.Imaging.ImageAttributes'
    $matrix.Matrix00 = '0.0'; $matrix.Matrix10 = '1.0'; $matrix.Matrix11 = '1.0'; $matrix.Matrix12 = '1.0'; $matrix.Matrix22 = '0.0'; $matrix.Matrix33 = '0.5'
    $imgAttrib.SetColorMatrix($matrix, [System.Drawing.Imaging.ColorMatrixFlag]::Default, [System.Drawing.Imaging.ColorAdjustType]::Bitmap)
    $graphics.DrawImage($Image, (New-Object 'System.Drawing.Rectangle'(0, 0, $Image.Width, $Image.Height)), 0, 0, $newImage.Width, $newImage.Height, [System.Drawing.GraphicsUnit]::Pixel, $imgAttrib)
    Return $newImage

# Custom draws each cmoComboBox item
$ComboIcons_OnDrawItem = {
    [System.Windows.Forms.DrawItemEventArgs]$e = $_

    [System.Drawing.Rectangle]$bounds = $e.Bounds    
    If (($e.Index -gt -1) -and ($e.Index -lt $IconComboItems.Count))
        $currItem = $IconComboItems[$e.Index]

        [int]                      $indent     = ($currItem.Indent * 14) 
        [System.Drawing.Image]     $icon       = $imgResult.Images[$currItem.Icon]
        [System.Drawing.SolidBrush]$solidBrush = [System.Drawing.SolidBrush]$e.ForeColor

        If ($currItem.Enabled -eq $False) {
            $icon = (GreyScaleImage -Image $icon)
            $solidBrush.Color = [System.Drawing.SystemColors]::GrayText

        $iconRect = New-Object 'System.Drawing.RectangleF'((($bounds.Left) + 5 + $indent), (($bounds.Top) + 2), 16, 16)
        $textRect = New-Object 'System.Drawing.RectangleF'((($bounds.Left) + ($iconRect.Width) + 9 + $indent), $bounds.Top, (($bounds.Width) - ($iconRect.Width) - 9 - $indent), $bounds.Height)
        $format   = New-Object 'System.Drawing.StringFormat'
        $format.Alignment     = [System.Drawing.StringAlignment]::Near
        $format.LineAlignment = [System.Drawing.StringAlignment]::Center

        If ($icon -ne $null) { $e.Graphics.DrawImage($icon, $iconRect) }
        $e.Graphics.DrawString($currItem.Text, $e.Font, $solidBrush, $textRect, $format)

# Get the currently selected item (but only if not disabled)
$ComboIcons_SelectedIndexChanged = {
    If ($cmoComboBox.SelectedIndex -lt 0) { Return }
    If ($cmoComboBox.SelectedIndex -lt $IconComboItems.Count)
        If ($IconComboItems[$cmoComboBox.SelectedIndex].Enabled -eq $false) { $cmoComboBox.SelectedIndex = $script:ComboIcons_SelectedItem }
        Else                                                             { $script:ComboIcons_SelectedItem = $cmoComboBox.SelectedIndex }
    $lbl_Description.Text = $($IconComboItems[$script:ComboIcons_SelectedItem])

ComboxBox List Items

There is a little more to adding items to the ComboBox list however.  You need to create a new object, populate it your required values, then add that to the $IconComboItems array.  Once you have created all your items, add them to the ComboBox control

Single Item Example

# Define new custom ComboBox item
$newItem         = New-IconComboItem
$newItem.Name    = 'item01'
$newItem.Text    = 'This is Item 01'
$newItem.Icon    = 0
$newItem.Indent  = 0
$newItem.Enabled = $True

# Add the new item to the array of items

# Repeat the above until you have added all the items you need

# Add the array of items to the ComboBox itself
$cmoComboBox.SelectedIndex = 0

Coloured Folder Example (Screen Shot 1)

[string[]]$colors = @('Black', 'Blue', 'Cyan', 'Green', 'Grey', 'Orange', 'Pink', 'Red', 'White', 'Yellow')
0..9 | ForEach-Object -Process {
    $tmp = New-IconComboItem
    $tmp.Name = "Item$_"
    $tmp.Text = $colors[$_]
    $tmp.Icon = $_
    If ($_ -gt 0) { $tmp.Indent = (Get-Random -Maximum 2 -Minimum 0) }

    $tmp = $null

$cmoComboBox.SelectedIndex = 0



Obviously there is a lot of code missing that is used in the screen shots, but the important part is there in order to get you up and running.  Have a play, and let me know what you think in the comments below.

A full demo can be found on my GitHub

OwnerDraw ListView


I am writing a small PowerShell tool that will perform a few checks against several virtual machines in vCenter.  I wanted to show the results in a nice grid view.  The ListView control is the easy choice for this.


Basic Grid

This picture below shows the basic layout that I started with (with random dummy data), and as you can see it’s not great.

The ‘P’ass and ‘F’ail display could be better, icons would be great.  The problem is that the basic ListView does not support more than one icon in the grid – usually the first one next to the name.

There are some tweaks you can do to move the single icon about by changing columns, but it’s still just one icon.


With a lot of the Windows forms controls they support an OwnerDraw method.  This allows developers to hack around with controls and make them do things that they can’t normally do – like having more that one icon in a ListView, including its subitems.  🙂

Looks great doesn’t it.!

It works by replacing “P” or “F” in the top screenshot with “ICON|x” where “x” is the index number of the image you want to use.  For example, my ticks are in position 1, and the crosses are position 2.  Therefore “ICON|1” or “ICON|2” would be used.  My code has an additional option of displaying an icon and text in the same box.  To show this use “BOTH|x|text”

The Code

In order to change a standard ListView into an awsome ListView does not require a lot of code.

about 50 lines of additional code for the OwnerDraw part, and 3x lines in the middle of the ListView definition.

ListView Definition

# Define ListView Control
$lstList                      = New-Object 'System.Windows.Forms.ListView'

# Add OwnerDraw Code
$lstList.OwnerDraw            = $True

# Set ListView properties
$lstList.Location             = ' 43,  12'
$lstList.Size                 = '589, 208'
$lstList.LabelEdit            = $True
$lstList.FullRowSelect        = $True
$lstList.GridLines            = $True
$lstList.HideSelection        = $True
$lstList.HeaderStyle          = 'Nonclickable'
$lstList.SmallImageList       = $imgResult
$lstList.Sorting              = 'Ascending'
$lstList.View                 = 'Details'

# Add first two columns
$lstList_CH_1                 = New-Object 'System.Windows.Forms.ColumnHeader'
$lstList_CH_2                 = New-Object 'System.Windows.Forms.ColumnHeader'
$lstList_CH_1.Text            = 'Virtual Machine Name'
$lstList_CH_2.Text            = 'Status'
$lstList_CH_1.Width           = 244
$lstList_CH_2.Width           = 100
$lstList_CH_2.TextAlign       = 'Center'
$lstList.Columns.Add($lstList_CH_1) | Out-Null
$lstList.Columns.Add($lstList_CH_2) | Out-Null

# Add 'results' columns
For ($i=0; $i -le 7; $i++)
    $newCH           = New-Object 'System.Windows.Forms.ColumnHeader'
    $newCH.Text      = 'RESULTS'.Substring($i, 1)
    $newCH.Width     = 32
    $newCH.TextAlign = 'Center'
    $lstList.Columns.Add($newCH) | Out-Null

# Add control to the main form

Most of this code you should already know if you write PowerShell Forms.  Lines 05,06 and 07 are the new ones that you need for the OwnerDraw parts.

OwnerDraw Code

This next code block has two functions, they are explained below.

$SubIcons_DrawColumnHeader = {
    [System.Windows.Forms.DrawListViewColumnHeaderEventArgs]$e = $_
    $e.DrawDefault = $True

$SubIcons_DrawSubItem = {
    [System.Windows.Forms.DrawListViewSubItemEventArgs]$e = $_

    If ($e.SubItem.Text.Length -le 5) { $e.DrawDefault = $True }
        If ($e.SubItem.Text.Contains('|') -eq $True)
            [System.Drawing.Image]$icon = ($imgResult.Images[$e.SubItem.Text.Split('|')[1] -as [int]])
            [int]$xPos = ($e.SubItem.Bounds.X + (($e.SubItem.Bounds.Width  / 2) -as [int]) - (($icon.Width  /2) -as [int]))
            [int]$yPos = ($e.SubItem.Bounds.Y + (($e.SubItem.Bounds.Height / 2) -as [int]) - (($icon.Height /2) -as [int]))

        Switch ($e.SubItem.Text.Substring(0,5).ToUpper())
                $r = New-Object 'System.Drawing.Rectangle'($xPos, $yPos, $icon.Width, $icon.Height)

                $e.DrawDefault = $false
                $e.Graphics.DrawImage($icon, $r)

                [int]$fPos = ((($e.SubItem.Bounds.Height - $sysFont.Height) / 2) -as [int])
                $fColor    = New-Object 'System.Drawing.SolidBrush'($e.SubItem.ForeColor)

                $rI = New-Object 'System.Drawing.Rectangle'($($e.SubItem.Bounds.X + 3), $yPos, $icon.Width, $icon.Height)
                $rS = New-Object 'System.Drawing.PointF'   ($($e.SubItem.Bounds.X + 5 + $icon.Width), $($e.SubItem.Bounds.Y + $fPos))

                $e.DrawDefault = $false
                $e.Graphics.DrawImage($icon, $rI)
                $e.Graphics.DrawString($e.SubItem.Text.Split('|')[2], $sysFont, $fColor, $rS)

            Default { $e.DrawDefault = $True }

The first SubIcons_DrawColumnHeader can largely be ignored.  It’s needed to draw the default column headers.

The second is the main worker.  It turns the text “ICON|x” into the image to be used, and displays it centred in the subitem window.  It also displays “BOTH|x|text” with the icon on the left with the text left-aligned next to it.


Obviously there is a lot of code missing that is used in the screen shots, but the important part is there in order to get you up and running.  Have a play, and let me know what you think in the comments below.

A full demo can be found on my GitHub

Server QA Scripts – Settings Configuration Tool


Following on from my Server QA Scripts post, I have since created a GUI form to make the configuration a lot easier to do.  The tool is fully written in PowerShell and is uploaded to my GitHub page.

To Use


To use, just open a PowerShell windows and start the `QA-Settings-Configurator.ps1` script.

You’ll be presented with the following window.

QA GUI Page 1

Click the Set Check Location button and location the folder where your scripts are located.  Once done, you will be able to change the language (if any) and a base settings file (if any others exist).  Click Import Settings when done.

The script will then scan the scripts folder and load the selected settings file.

Select Required Checks

The next page lists all the checks available and automatically selects all the checks enabled in the settings file.

QA GUI Page 2

You can select or deselect any check that you feel best suits your environment.  For example, if you don’t use McAfee Antivirus in your environment, you wouldn’t want to check for the McAfee Antivirus agent, therefore, you would deselect the COM01 check.

Once you have finished your selections, click the Next > button.

QA Check Values

The third page has several tabs, one tab for each of the check sections.  Within each of the these tabs is a list of the checks you selected, as well as the required settings for each check.

QA GUI Page 3

Double-click each of the settings and change the values that you require for your environment, remembering to check the settings in each of the tabs.

When you are happy with your settings, select the final page.

Generate QA

QA GUI Page 4

Now that you have completed your changes, enter a short code for your QA script file.  This will be the unique code that the QA compiler will use to name your QA script file.  For example, a short code of “ACME” will generate a QA script called “QA_ACME_v3.xx.xxxx.ps1“.  Also enter a company name for the HTML reports that are generated.

Click Save Settings when you are ready, and enter a filename to save your settings as.  Generate a compiled QA script from this page too by clicking the Generate QA Script button.


Other functions

If you happen to loose your settings file, you can restore them by clicking the Restore Settings File button.  Select the location of a compiled QA script and the function will recreate the settings INI file for you.


Server QA Scripts


At the start of 2015 my company was still building a lot of new Windows servers manually (physical and virtual).  Each of these servers had a check sheet of what was completed and what wasn’t.  Sometimes stuff would be missed for one reason or another, which is why the servers typically went through a process of QA (Quality Assurance).

This QA check was a time consuming manual process that involved someone logging into each server and checking that everything was built and configured as it should be, as well as making sure all the correct tooling was installed (AV, SCOM and SCCM agents, etc).  This typically took a good 2 hours per server.  Results were filled in to an Excel spreadsheet and filed away somewhere as an audit trail.


My Solution

Fast forward to today, and those servers are now mostly built via our in-house automation process, however there is still some manual configuration that is required.  The QA process has also improved.  I have taken the manual spreadsheet and converted it into separate PowerShell scripts that are ‘compiled’ into one large script, that is just shy of 8000 lines.!

This script takes less than a minute to complete over 70 checks and write a HTML report for each viewing

My scripts are currently at version 3 and have been released into the wild so that other people can take advantage of the hard work I  put in to create them.


The Scripts

I have spent the best part of 18 months creating and tweaking these scripts.  I started with a broken system of about 40 QA checks and got them working.  I then expanded and enhanced the number and quality of the scripts as well as the underlying engine.

As of today (2016-09-06) there are 75 checks.  Hopefully with some community input and help we can increase that.


GitHub Repository

All the code is available for anyone to play with on my GitHub repository:


Screen Shot


A full QA scan against one server









Out-FancyHTML Function


For a larger project of mine, I needed to produce some fancy looking HTML reports from the data I collected.  An example of this output can be seen below.:



The screen shot shows the results of the code below running on my local laptop:

Get-Process | Select Name, CPU, Handles, Path, Company, FileVersion | Sort Name



The code takes the results of any table based output, formats it as a HTML table, then runs various colour highlighting on specific cells depending on the rules you specify.

For example, the following code will find any cell in the “Handles” column and checks the values found.  If the value is between 50 and 100, it will be coloured in pale yellow (#FFFFC0)

$html = Set-CellColour -InputObject $html -Filter 'Handles -lt 100 -and Handles -gt 50' -Colour '#ffffc0'

Several colour filters can be applied as shown in the code below.



The following code will produce something similar, and will get you on your way, you can also download it below…

# Need to set some variables first....
[string]$dt1    = (Get-Date -Format 'yyyy/MM/dd HH:mm')
[string]$un     = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name.ToLower()
[string]$server = '[server/report name here]'
[string]$report = '[report name here]'

# Vaguely based on 
Function Set-CellColour
    Param ( [Object[]]$InputObject, [string]$Filter, [string]$Colour, [switch]$Row )
        $Property = ($Filter.Split(' ')[0])
        If ($Filter.ToUpper().IndexOf($Property.ToUpper()) -ge 0)
            $Filter = $Filter.ToUpper().Replace($Property.ToUpper(), '$value')
            Try { [scriptblock]$Filter = [scriptblock]::Create($Filter) } Catch { Exit }
        } Else { Exit }

        ForEach ($input In $InputObject)
            [string]$line = $input
            If ($line.IndexOf('<tr><th') -ge 0)
                [int]$index = 0
                [int]$count = 0
                $search = $line | Select-String -Pattern '<th>(.*?)</th>' -AllMatches
                ForEach ($match in $search.Matches)
                    If ($match.Groups[1].Value -eq $Property) { $index = $count }
                If ($index -eq $search.Matches.Count) { $index = -99; Break }

            If ($line -match '<tr><td')
                $line = $line.Replace('<td></td>','<td> </td>')
                $search = $line | Select-String -Pattern '<td(.*?)</td>' -AllMatches
                If (($search -ne $null) -and ($search.Matches.Count -ne 0) -and ($index -ne -99))
                    $value = ($search.Matches[$index].Groups[1].Value).Split('>')[1] -as [double]
                    If ($value -eq $null) { $value = ($search.Matches[$index].Groups[1].Value).Split('>')[1] }
                    If (Invoke-Command $Filter)
                        If ($Row -eq $true) { $line = $line.Replace('<td>', ('<td style="background:{0};">' -f $Colour)) }
                        Else {
                            [string[]]$arr = $line.Replace('><','>■<').Split('■')
                            If ($arr[$index + 1].StartsWith('<td'))
                                $arr[$index + 1] = $arr[$index + 1].Replace($search.Matches[$index].Value, ('<td style="background:{0};">{1}</td>' -f $Colour, $value))
                                $line = [string]::Join('', $arr)
            Write-Output $line

    { }

# CSS for the output table...
[string]$css = @'
    html body       { font-family: Verdana, Geneva, sans-serif; font-size: 12px; height: 100%; margin: 0; overflow: auto; }
    #header         { background: #0066a1; color: #ffffff; width: 100% }
    #headerTop      { padding: 10px; }
    .logo1          { float: left;  font-size: 25px; font-weight: bold; padding: 0 7px 0 0; }
    .logo2          { float: left;  font-size: 25px; }
    .logo3          { float: right; font-size: 12px; text-align: right; }
    .headerRow1     { background: #66a3c7; height: 5px; }
    .serverRow      { background: #000000; color: #ffffff; font-size: 32px; padding: 10px; text-align: center; text-transform: uppercase; }
    .sectionRow     { background: #0066a1; color: #ffffff; font-size: 13px; padding: 1px 5px!important; font-weight: bold; height: 15px!important; }
    table           { background: #eaebec; border: #cccccc 1px solid; border-collapse: collapse; margin: 0; width: 100%; }
    table th        { background: #ededed; border-top: 1px solid #fafafa; border-bottom: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0; height: 45px; min-width: 55px; padding: 0px 15px; text-transform: capitalize; }
    table tr        { text-align: center; }
    table td        { background: #fafafa; border-top: 1px solid #ffffff; border-bottom: 1px solid #e0e0e0; border-left: 1px solid #e0e0e0; height: 55px; min-width: 55px; padding: 0px 10px; }
    table td:first-child   { min-width: 175px; text-align: left; }
    table tr:last-child td { border-bottom: 0; }
    table tr:hover td      { background: #f2f2f2; }
    table tr:hover td.sectionRow { background: #0066a1; }

# Page header rows...
[string]$body = @"
<div id="header"> 
    <div id="headerTop">
        <div class="logo1">ACME</div>
        <div class="logo2">$report</div>
        <div class="logo3">&nbsp;<br/>Generated by $un on $dt1</div>
        <div style="clear:both;"></div>
    <div style="clear:both;"></div>
<div class="headerRow1"></div>
<div class="serverRow">$server</div>
<div class="headerRow1"></div>

# Get a list of processes, and convert to HTML... 
[string[]]$html = Get-Process | Select Name, CPU, Handles, Path, Company, FileVersion | Sort Name | ConvertTo-Html -Head $css -Body $body

# Colour some  cells depending on the filters, filters can contain any valid forumla...
$html = Set-CellColour -InputObject $html -Filter 'Handles -lt 100 -and Handles -gt 50' -Colour '#ffffc0'
$html = Set-CellColour -InputObject $html -Filter 'Handles -gt  99'                     -Colour '#ffc0c0'
$html = Set-CellColour -InputObject $html -Filter 'Handles -lt  51'                     -Colour '#c0ffc0'
$html = Set-CellColour -InputObject $html -Filter 'Name    -eq "ccmexec"'               -Colour 'Gray' -Row
$html = Set-CellColour -InputObject $html -Filter 'Name    -eq "chrome"'                -Colour '#c0c0ff'

# Output the entire HTML to a text file...
$html += '<table><tr><td class="sectionRow">&nbsp;</td></tr></table>'
$html | Out-File .\Out-FancyHTML_Result.html


Have a play, see what you think.  Either leave a comment below, or on the Reddit thread here.






One of the easiest ways to get a list of installed applications on a machine is to use the WMI class Win32_Product.  This enumerates every application.  For example, the following code will display a table with the name of the installed application and it’s version number:

Get-WmiObject -Class Win32_Product | Select Name, Version

Great, now let’s filter it to find a specific application.  I’ll search for any application I have installed with ‘zip’ in the name:

Get-WmiObject -Class Win32_Product -Filter { Name like "%zip%" } | Select Name, Version

Name                                       Version
----                                       -------
7-Zip 9.38 (x64 edition)         

Just what we wanted, but there is a problem.


If you now look at your Application event log, you will notice a huge list of entries that say something like

Windows Installer reconfigured the product. Product Name: [name]. Product Version: [version]. Product Language: 1033. Manufacturer: [manufacturer]. Reconfiguration success or error status: 0.

You will see one entry for every single application you have installed (and a few more).  You may also notice that the command above took quite a long time to run.  In some cases I have seen it take up to 5 minutes to return just one entry.

This is a well known and blogged about topic, Microsoft even has a Knowledge Base article on it: KB974524.



My solution to this problem is a PowerShell function that searches the registry for the same information.  This has the benefit of being incredibly fast.

It works by searching the two locations where applications put their uninstall details.



The first is for 32-bit applications, the second for 64-bit applications.

To use my function simply call it with the following command.  Again, searching for any application with ‘zip’ in the name:

Win32_Product -serverName $env:computername -displayName 'zip'

This will return just the version number :

My function will only return the version number of the first application it finds that matches the input value.  You could expand on this and return all application names and version numbers using a hashtable or an array.  I’ll leave that up to you.


$regSearch = 'Software\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
Function Win32_Product
    Param ([string]$serverName, [string]$displayName)
        $reg = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $serverName)
        $regKey = $reg.OpenSubKey($regSearch)
        If ($regKey) { [array]$keyVal = $regKey.GetSubKeyNames() }
    Catch { Return $null }

    $found = $false
    If (($regKey) -and ($keyVal.Count -gt 0)) {
        ForEach ($app In $keyVal) {
            $appKey = $regKey.OpenSubKey($app).GetValue('DisplayName')
            If ($appKey -like ('*' + $displayName + '*')) {
                $found = $true
                [string]$verCheck = $regKey.OpenSubKey($app).GetValue('DisplayVersion')
                If (-not $verCheck) { $verCheck = '0.1' } }
        If ($found -eq $false) {
            If ($regSearch -like '*Wow6432Node*') {
                $regSearch = $regSearch.Replace('Wow6432Node', '')
                $verCheck = Win32_Product -serverName $serverName -displayName $displayName
            Else { $verCheck = $null } }
    Else { $verCheck = $null }
    Return $verCheck