Powershell: Find Installed MSI’s on Remote Machines

As an admittedly lazy IT person I’m all about crafting ways to automate tasks, even if it requires a bit more forethought on the frontend. I had a request come up to do an “inventory” of who had a certain software package on our machines. After determining that the Win32_Product WMI class could return a list of .msi-based installs on the machine (as well as the slew of handy metadata that comes with it), I started crafting up a script that would go out, target a specific machine that I wanted to find out if a particular package was on it, or, given my fancy, search all the machines in the domain. This is what I eventually came up with. Oh – some standard boilerplate:

The following code works fine in my environment. Mileage may vary in your own; modification may be necessary to suit your needs. Nobody, including myself, accepts any liability for any action or consquence resulting in the execution of the following code.

# Powershell - Domain Computer Software Install-base Search

#####INITIAL PARAMETERS AND GLOBAL VARIABLES#####
##################################################

########FUNCTIONS#########
# This is a function that will grab a list of computers out of Active Directory. Yanked from another script of Keith's.
Function List-Computers
{
    $strCategory = "computer"

    $objDomain = New-Object System.DirectoryServices.DirectoryEntry

    $objSearcher = New-Object System.DirectoryServices.DirectorySearcher
    $objSearcher.SearchRoot = $objDomain
    $objSearcher.Filter = ("(objectCategory=$strCategory)")

    $colProplist = "name"
    foreach ($i in $colPropList){
        $objSearcher.PropertiesToLoad.Add($i)
    }

    $colResults = $objSearcher.FindAll()

    foreach ($objResult in $colResults){
        $objComputer = $objResult.Properties; $objComputer.name
    }
}
#############################

$ComputerName = Read-Host "Enter target machine (if blank, all are selected)"
# If the field is blank, run a List-Computers function, then sort the list ascending and store that
# array as the variable.
if ($ComputerName -eq ""){
    $ComputerName = List-Computers | Sort-Object
    }

# Entering a keyword that will be used to search in the description and title strings of the
# software packages.
$SoftQuery = Read-Host "Please enter a keyword to search for:"
$SoftQuery = $SoftQuery.toUpper() #Convert anything typed to uppcase for later matching.

# The following if-then-else pings the desired machine; if not connected, it displays the
# hostname followed by "NOT CONNECTED". If they are connected, it runs the remainder of the search.
$ComputerName | foreach{
	$tmp = ping -n 1 $_;
	if ($LASTEXITCODE -eq "1"){
		Write-Host $_ "- NOT CONNECTED"
	}
    else{
        $Product = gwmi Win32_Product -ComputerName:$_
        # Parse the results for the software you're after and display each query-match result
        # for each machine
        Write-Host `n
        Write-Host Matches for $_
        Write-Host ======================================
        $Product | foreach {
            # Sets the location for relevant documentation to the Help Link
            # or About URL if it exists - can't remember where I got this.
            if ($_.HelpLink -eq $null){
                if($_.URLInfoAbout -eq $null){
                    $Documentation = $null
                }
                else{
                    $Documentation = $_.URLInfoAbout
                }
            }
            else{
                $Documentation = $_.HelpLink
            }

        # Set the variables
        $ProdName = $_.Caption
        $SoftMatch = $ProdName.ToUpper().Contains($SoftQuery)

        #I didn't want it wasting time (oh the split-seconds) enumerating variables if
        # there's no match, so it's in the if statement.
        if ($SoftMatch -eq "True"){
            $ProdInstallDateInit = $_.InstallDate2
                if($ProdInstallDateInit -eq $NULL){$ProdInstallDate = "Unknown"}
                else{$ProdInstallDate = $_.InstallDate2.Substring(4,2) + "/" + $_.InstallDate2.Substring(6,2) + "/" + $_.InstallDate2.Substring(0,4)}
        if($_.InstallLocation -eq $NULL){$ProdInstallPath = "Unknown"}
        else{$ProdInstallPath = $_.InstallLocation}
        if($_.PackageCache -eq $NULL){$ProdPackageCache = "Unknown"}
        else{$ProdPackageCache = $_.PackageCache}
        if($_.Vendor -eq $NULL){$ProdVendor = "Unknown"}
        else{$ProdVendor = $_.Vendor}
        if($_.Version -eq $NULL){$ProdVersion = $_.Version}
        else{$ProdVersion = $_.Version}
        switch ($_.InstallState){
            -6 {$ProdState = "Bad Configuration"}
            -2 {$ProdState = "Invalid Argument"}
            -1 {$ProdState = "Unknown Package"}
            -1 {$ProdState = "Advertised"}
            2 {$ProdState = "Absent"}
            5 {$ProdState = "Installed"}
            default {$ProdState = "Unknown"}
        }
    }

    if ($SoftMatch -eq "True"){
        Write-host "-------------------"
        Write-Host "Name:         " $ProdName
        Write-Host "Vendor:       " $ProdVendor
        Write-Host "Version:      " $ProdVersion
        Write-Host "Install Date: " $ProdInstallDate
        Write-Host "Install Path: " $ProdInstallPath
        Write-Host "Package Path: " $ProdPackageCache
        Write-Host "Package State:" $ProdState
        Write-Host "-------------------"
    }
}
Write-Host ======================================
Write-Host `n
}
}

For example if I run on a target of my machine looking for “Adobe”, this is the output it brings back after enumerating my software list and checking the titles/desciptions:

Matches for COMPUTERNAME
======================================
-------------------
Name:          Adobe Reader 9.3
Vendor:        Adobe Systems Incorporated
Version:       9.3.0
Install Date:  Unknown
Install Path:  C:\Program Files\Adobe\Reader 9.0\Reader\
Package Path:  C:\Windows\Installer\a78edc.msi
Package State: Installed
-------------------
======================================

The only drawback to this is that by default Windows Server 2003 and Windows XP Pro x64 clients do not include the Win32_Product Class. So, if you’re dealing with any 64-bit clients you’ll have to get that class installed. Luckily, some internet searching turned up this wonderful article that describes a batch file and gives a Powershell script on how to do this with a supplied text file of clients. Looks like s/he, too, has written a software finding script.

Of course, I could have went the extra mile (and might, someday) to do a test after the ping that checks if they’re 64-bit or not, then checks to see if Win32_Product is installed an, if not, install it. I suppose you could also use this to check if people have up-to-date versions of software by checking against a known up-to-date machine’s version number… but that was more than I needed to do and running it separately sufficed.

If you have any input or comments on this script please feel free to drop ‘em below.

Did you find this article useful? Pass it on!

  1. if ($_.InstallState -eq -6){$ProdState = "Bad Configuration"}
    elseif($_.InstallState -eq -2){$ProdState = "Invalid Argument"}
    elseif($_.InstallState -eq -1){$ProdState = "Unknown Package"}
    elseif($_.InstallState -eq -1){$ProdState = "Advertised"}
    elseif($_.InstallState -eq 2){$ProdState = "Absent"}
    elseif($_.InstallState -eq 5){$ProdState = "Installed"}
    else{$ProdState = "Unknown"}

    long blocks of if/elseif/else statements should be written as a switch; this saves you from a lot of typing and looks cleaner. The above block could be rewritten as:


    switch ($_.InstallState){
    -6 {$ProdState = "Bad Configuration"}
    -2 {$ProdState = "Invalid Argument"}
    -1 {$ProdState = "Unknown Package"}
    -1 {$ProdState = "Advertised"}
    2 {$ProdState = "Absent"}
    5 {$ProdState = "Installed"}
    default {$ProdState = "Unknown"}
    }

    Pretty cool regardless; you just wait, we’ll make a programmer out of you yet xD

  2. I updated the code to reflect the switch. Does look better; it’s been awhile.

Leave a Comment