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.
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
I updated the code to reflect the switch. Does look better; it’s been awhile.