Getting a per-system software inventory

This latest script is a bit of a swerve from my last post, but I was reading about PowerShell providers recently and wanted to experiment with the Registry provider, since in the past I’ve learned the hard way that any significant amount of registry work in batch files is painful at best.

My intention with this script is to generate a CSV file containing details of all software registered with the Windows Installer in the registry. This can be useful not just for tracking installed software in environments where centralised systems for doing this are not feasible (whether  for organisational size or budgetary reasons), but also for identifying out-dated or licencing-restricted software that requires the attention of an administrator. At some point in the near future I’ll write another script that processes the output of this script and a set of rules provided by the administrator to take required actions, which can include removing unauthorised software, installing an updated software release or notifying the administrator of specific installed packages. Beyond that, my idealised version would be tied into a network resource (be it a network share or a webserver) from which updated rulesets and installation media could be derived.

The fundamentals of this script are to iterate through the Uninstall registry key subkeys (for both 32-bit and 64-bit software installs) to get a list of installed software and related details. The principles used in this script can also be used to update the registry information for software installers which don’t follow best practice (eg developers who think that putting the version number in the DisplayName is a good idea). There are other ways around this (such as using Orca to edit MSI installation packages) but they are not always available, and in some cases (such as requiring signed installation packages) it’s better to just modify the registry information for the installed software than to change the installed package.

One thing to note is that if you switch from the Environment provider to the Registry provider, you need to switch back to the Environment provider to use file-based cmdlets like Out-File. This means that if you interrupt the script before it finishes, the ISE or PowerShell session will most likely still have its location set as the Registry provider. That’s enough for now – let’s get on with looking at the script itself.

function Get-InstalledSoftware {
    # Set the location as the registry HKLM node:
    Set-location HKLM:

    # Initialise the hashtable
    $global:swlist=$null
    $global:swlist= @{}

    # Define the paths for software installations and the methods for retrieving a list of installed software under those paths
    $32bitswpath = "Software\WoW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
    $32bitswlist = gci -path $32bitswpath
    
    # Iterate through items in the list of 32-bit installation registry keys, extracting the properties we want 
    $i= 0
    do {
        # Retrieve the properties we need:
        $currobj = (Get-ItemProperty -path $32bitswlist[$i])
        if ($currobj.DisplayName) {
            $displayname = $currobj.DisplayName.ToString()
        }
        if ($currobj.DisplayVersion) {
            $displayversion = $currobj.DisplayVersion.ToString()
        }
        if ($currobj.InstallLocation) {
            $installlocation = $currobj.InstallLocation.ToString()
        }
        if ($currobj.UninstallString) {
            $uninstallstring = $currobj.UninstallString.ToString()
        }
        # In case of entries with null values for the above, also grab the PSChildName attribute
        $psName = $32bitswlist[$i].PSChildName

        # Assign the properties to a new row in the array
        try {
            $global:swlist.Add("$i",@{"Registry Key Name" = $psName; "DisplayName" = $displayname;"DisplayVersion" = $displayversion;"InstallLocation" = $installlocation;"UninstallString" =$uninstallstring})
            Clear-Variable -Name psName,displayname,displayversion,installlocation,uninstallstring -force -ErrorAction SilentlyContinue
        } catch {
            Write-Host "Unexpected problem getting item properties for path "$32bitswlist[$i]
            WRite-Host "Error message was" $_.Exception.Message
            Write-Host " "
        }
        $i++
    } while ($i -lt $32bitswlist.count)

    if ($OStype -eq "64-bit") {
        $64bitswpath = "Software\Microsoft\Windows\CurrentVersion\Uninstall"
        $64bitswlist = gci -path $64bitswpath
    
        # Repeat for items in the list of 64-bit installation registry keys, extracting the properties we want. Since we're using the counter as an index in the hashtable, we don't reset $i to zero but instead add a second counter based on it.
        do {
            $j = ($i - $32bitswlist.count)
            # Retrieve the properties we need:
            $currobj = (Get-ItemProperty -path $64bitswlist[$j])
            if ($currobj.DisplayName) {
                $displayname = $currobj.DisplayName.ToString()
            }
            if ($currobj.DisplayVersion) {
                $displayversion = $currobj.DisplayVersion.ToString()
            }
            if ($currobj.InstallLocation) {
                $installlocation = $currobj.InstallLocation.ToString()
            }
            if ($currobj.UninstallString) {
                $uninstallstring = $currobj.UninstallString.ToString()
            }
            # In case of entries with null values for the above, also grab the PSChildName attribute
            $psName = $64bitswlist[$j].PSChildName

            # Assign the properties to a new row in the array
            try {
                $swlist.Add("$i",@{"Registry Key Name" = $psName; "DisplayName" = $displayname;"DisplayVersion" = $displayversion;"InstallLocation" = $installlocation;"UninstallString" =$uninstallstring})
            } catch {
                Write-Host "Unexpected problem getting item properties for path "$64bitswlist[$j]
                WRite-Host "Error message was" $_.Exception.Message
                Write-Host " "
            }
            Clear-Variable -Name j,psName,displayname,displayversion,installlocation,uninstallstring -force -ErrorAction SilentlyContinue
            $i++
        } while (($i - $32bitswlist.count) -lt $64bitswlist.count)
    }
#>
}

function Export-SoftwareList ($swlist) {
    # Define the output file name, using both the local computer's hostname and the date & time at which the file is created.
    $output = "$source"+"\"+"Installed_Software_"+$env:Computername+"_"+"$OStype"+"_"+(Get-date -format yyyyMMddHHmm)+".csv"

    # Define the desired headers for the CSV output file
    $headers = "Index,Registry Key Name,Display Name,Display Version,Install Location,Uninstall String"

    # Output the headers to the file
    $headers | Out-File $output -append

    # Iterate through each entry in the swlist hashtable, creating a comma-separated string that can be piped to the output file.
    $k=0
    do {
        $reg=$swlist["$k"]["Registry Key Name"]
        $dispname=$swlist["$k"]["DisplayName"]
        $dispver=$swlist["$k"]["DisplayVersion"]
        $instloc=$swlist["$k"]["InstallLocation"]
        $uninst=$swlist["$k"]["UninstallString"]

        "$k"+","+$reg+","+$dispname+","+$dispver+","+$instloc+","+$uninst | Out-File $output -append
        $k++
    } while ($k -lt $swlist.count)
}

# Capture the starting location & PSProvider for the script
$source=(Get-Location).Path

# Capture the OS architecture
$OStype = ((Get-WMIObject -Class win32_OperatingSystem -ComputerName $env:ComputerName).OSArchitecture)

# Call the inventory functions
Get-InstalledSoftware
Export-SoftwareList $swlist

# Switch back to the starting location & PSProvider
set-location $source

One thought on “Getting a per-system software inventory

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.