How to inventory server software with PowerShell

ByLance T. Lee

Mar 2, 2022

Being able to quickly identify the software installed on your servers is valuable for many reasons. Managing software license costs and rights, planning upgrade budgets, identifying candidates for server consolidation, or even responding to security incidents are all common reasons for conducting a software inventory.

There are, of course, enterprise tools for tracking software inventory. But these tools can be expensive and complex, or may have limited access to specific groups or individuals in your organization. Fortunately, PowerShell can help you with some of the software analysis work on your systems to help you plan and respond to incidents.

Sometimes you also need to go deeper than just listing installed software. Getting details about things like startup services or applications, file shares, or even open ports can be important to fully answer your business questions.

Identification of installed software

In theory, retrieving a list of installed software is a straightforward proposition. By using the Get-WmiObject The cmdlet to query the Win32_Product class produces a list of installed software, but in many cases you will find that this list is incomplete. A better alternative is Get-Package cmdlet, which offers several handy features.

ferrill powershell server software get the package Tim Ferrill

Functioning Get-Package without parameters will return a list of installed software, including Windows Updates, which is probably more detailed than what you’re looking for. To focus only on installed apps, you’ll want to exclude packages based on Microsoft Update (MSU) provider type using Get-Package -ProviderName Programs, MSI. Get-Package gives you the default software name and version, plus a few other fields you probably don’t care about, with additional fields hidden by default. Other key fields you may be interested in include the FullPath property, indicating where the application is installed, and the FromTrustedSource flag, which indicates whether the software was installed from a company-trusted software repository.

There are a few bits of useful data that aren’t readily available using Get-Package, but they are sometimes accessible if you are willing to dig a little below the surface. The SwidTagText property contains XML data that includes information such as a help phone number for the software, a referral URL, and even the installation date. Accessing these attributes takes a bit of gymnastics, but here’s how you can make it work:

Get-Package -ProviderName Programs, MSI | ForEach-Object {
    $xml = [xml]$_.SwidTagText
    $meta = $xml.SoftwareIdentity.Meta
    $_ | Add-Member -MemberType NoteProperty -Name HelpTelephone -Value $meta.HelpTelephone -PassThru
    $_ | Add-Member -MemberType NoteProperty -Name UrlInfoAbout -Value $meta.UrlInfoAbout -PassThru
} | Format-Table Name, Version, HelpTelephone, UrlInfoAbout -AutoSize

There’s quite a bit going on here, so let’s break down what’s going on.

The first line executes the Get-Package cmdlet and pipes the result to For each objectwhich then iterates through each element and executes the code contained within the wavy brackets.

In the loop code, the first step is to parse the XML string contained in the SwidTagText property.

Working backwards on this first line of the loop, $_references the current element in the ForEach loop, so $_.SwidTagText is going to reference this XML text for each software from Get-Package. Prefix the $_.SwidTagText with [xml] indicates that we don’t want to treat it as a text string, but as an XML object. Once we’ve done the work of getting the XML object, we assign it to a variable for easy reference in the future.

Once we have an XML object to work with, we want to navigate that object to the metadata, which we do with the $xml.SoftwareIdentity.Meta syntax, and we assign that component of the XML object to the $meta variable.

The next two lines are nearly identical, so we’ll only cover the concept once. Starting with the variable $_ which refers to the current instance of Get-Package, we pipe it to the Add a member cmdlet. Add a member allows us to add a new property by specifying the member type, name and value. The -PassThru flag simply persists the output of this Add a member cmdlet. Finally, we channel the result of the magic contained in the For each object block of code at Format-Table cmdlet, which simply gives us a table view of the specified columns. This last component could just as well use the Export-CSV Cmdlet to save the output for further analysis.

Identify application services and scheduled tasks

It’s one thing to be able to identify the software installed on your servers, but it’s also useful to know what services or scheduled tasks a software package has enabled on your systems. Of course, while both methods are used to execute code without user intervention, the two are markedly different in terms of how and when that code is executed as well as handled.

Services are relatively simple to explore with PowerShell because there is a Get-Service cmdlet. But as has been a recurring theme, Get-Service has some limitations. A better option is to use the Win32_Service WMI (Windows Management Instrumentation) class and the Get-WmiObject cmdlet.

Run something like Get-WmiObject Win32_Service | Where-Object Name -like ‘servicename*’ | Select-Object * will return all properties of the service you targeted. These properties include items such as name, description, and display name, as well as current state and startup type. Additionally, the PathName property identifies the process and arguments used to launch the service, and the ProcessId property identifies the current process in memory if the service is running.

ferrill powershell server software service properties Tim Ferrill

Scheduled tasks are a bit more difficult to query. The WMI Win32_ScheduledJob class will allow you to query jobs created by script or AT.exe, but not those created in the Scheduled Jobs control panel. You also have the option of using a whole set of classes in the TaskScheduler namespace if you want to go crazy. You can get a list of these classes using Get-WmiObject -List -Namespace Root/Microsoft/Windows/TaskScheduler. A simpler option is the Get-ScheduledTask cmdlet, which has a lot of detail buried below the surface.

To start, Get-ScheduledTask returns TaskName, TaskPath and State without digging. These properties give the friendly name you see in the Scheduled Tasks control panel, the task path in the navigation pane, and the task status.

Details like which app or command is running require a bit more research. The scheduled task stores this information under the Actions property in the Execute and Arguments properties. The following code snippet queries the list of tasks that are not in disabled state, extracts the action details and combines them into a new property named Command, then drops the output to the console.

Get-ScheduledTask | Where-Object State -ne Disabled | ForEach-Object { 
    $_ | Add-Member -MemberType NoteProperty -Name Command -Value ($_.Actions.Execute + ' ' + $_.Actions.Arguments) -PassThru
} | Select-Object TaskName, State, Command, TaskPath

In terms of key information for scheduled tasks, that’s a good start, but what if you want to get scheduling details. In the Scheduled Tasks control panel is the Next Run Time and Last Run Time, which is fine for summary purposes, but Get-ScheduledTask does not provide these details directly. The good news is that we don’t have to dig too deep for execution details, because the Get-ScheduledTaskInfo cmdlet gives us what we need.

To add the execution details to our code snippet above, we first need to get the task information details for each task when we loop through them with For each object. The easiest way to do this is to just pipe the current task instance to the Get-ScheduledTaskInfo cmdlet using built-in $_ variable. Once we’ve done that, we’ll assign it to the $taskInfo variable and then add the execution details to the output.

Get-ScheduledTask | Where-Object State -ne Disabled | ForEach-Object {
    $taskInfo = $_ | Get-ScheduledTaskInfo
    $_ | Add-Member -MemberType NoteProperty -Name Command -Value ($_.Actions.Execute + ' ' + $_.Actions.Arguments) -PassThru
    $_ | Add-Member -MemberType NoteProperty -Name LastStart -Value $taskInfo.LastRunTime -PassThru
    $_ | Add-Member -MemberType NoteProperty -Name NextStart -Value $taskInfo.NextRunTime -PassThru
} | Select-Object TaskName, State, Command, TaskPath, LastStart, NextStart

Now that we’ve established how to access the key details of our tasks, let’s return to the original purpose of collecting task data related to specific software tools.

There are a handful of properties that might contain useful details for correlating with an application. Some of these properties are available as parameters with Get-ScheduledTask, while others can only be used to filter results after tasks have already been queried. The -TaskName and -TaskPath parameters can be used to query these properties and can even use wildcards to find matches that aren’t exact.

The other piece of information that might be useful for research is the Run value under Actions, which has the potential to contain a file path or at least an executable. To query based on the Execute value, we can use Where-Object like that:

Get-ScheduledTask | Where-Object { $_.Actions.Execute -like ‘*Adobe*’ }

From there, the results can be piped to the rest of the code snippets we expanded above to fully expand the dataset and produce a usable result.

Join the Network World communities on Facebook and LinkedIn to comment on topics that matter to you.

Copyright © 2022 IDG Communications, Inc.

Source link