- Buy Microsoft Visio Professional or Microsoft Project Professional 2024 for just $80
- Get Microsoft Office Pro and Windows 11 Pro for 87% off with this bundle
- Buy or gift a Babbel subscription for 78% off to learn a new language - new low price
- Join BJ's Wholesale Club for just $20 right now to save on holiday shopping
- This $28 'magic arm' makes taking pictures so much easier (and it's only $20 for Black Friday)
How to inventory server software with PowerShell
Being able to quickly identify what software is installed on your servers has value for a host of reasons. Managing software licensing costs and entitlements, planning upgrade budgets, identifying candidates for server consolidation, or even responding to security incidents are all common reasons for performing a software inventory.
There are of course enterprise tools for tracking software inventory. But these tools can be expensive and complex, or could have access limited to specific groups or individuals in your organization. Fortunately PowerShell can help with some of the leg work in analyzing the software on your systems in order to help drive your planning and incident response.
Sometimes you also need to take things further than simply listing installed software. Acquiring details on things like services or startup apps, file shares, or even open ports can be important to fully answering your business questions.
Identifying installed software
In theory retrieving a list of installed software is a straightforward proposition. Using the Get-WmiObject cmdlet to query the Win32_Product class does produce a list of installed software, but in many cases you’ll find this list to be incomplete. A better alternative is the Get-Package cmdlet, which offers several handy capabilities.
Running Get-Package with no parameters will return a list of installed software including Windows updates, which is likely more detail than you’re after. To focus just on installed applications you’ll want to exclude packages based on the MSU (Microsoft Update) provider type by using Get-Package -ProviderName Programs, MSI. Get-Package gives you the software name and version by default, as well as a couple other fields you probably don’t care about, with additional fields hidden by default. Other key fields you may care about include the FullPath property, denoting where the application is installed, and the FromTrustedSource flag, which indicates whether the software was installed from a software repository that is trusted by the enterprise.
There are a few useful bits of data that aren’t easily available using Get-Package, but they are sometimes accessible if you are willing to dig below the surface a little bit. The SwidTagText property contains XML data which includes information like a help telephone number for the software, a reference URL, and even the install date. Accessing these attributes requires a little 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 happening.
The first line runs the Get-Package cmdlet and pipes the result to ForEach-Object, which then iterates through each element and executes the code contained within the squiggly brackets.
Within the loop code, the first step is to parse the XML string contained in the SwidTagText property.
Working backwards on that first line in the loop, $_references the current element in the ForEach loop, so $_.SwidTagText is going to refer to that XML text for each piece of software coming from Get-Package. Prefixing the $_.SwidTagText with [xml] indicates that we don’t want to treat it as a string of text, but as an XML object. Once we’ve done the work of getting the XML object, we assign that to a variable to make it easier to reference moving forward.
After we have an XML object to work with we want to navigate that object down to the metadata, which is what we’re doing 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 just cover the concept once. Starting with the $_ variable that references the current instance from Get-Package, we pipe that to the Add-Member cmdlet. Add-Member allows us to append a new property by specifying the member type, name, and value. The -PassThru flag simply retains the output of that Add-Member cmdlet. Finally, we pipe the result of the magic contained in the ForEach-Object code block to the Format-Table cmdlet, which simply gives us a tabular view of the specified columns. This final component could just as easily 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 what software is installed on your servers, but it’s also helpful 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 distinctly different in terms of how and when that code is executed as well as how they are managed.
Services are relatively straightforward to dig into with PowerShell since there’s 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.
Executing something like Get-WmiObject Win32_Service | Where-Object Name -like ‘servicename*’ | Select-Object * will return all the properties from the service you have targeted. These properties include things like the name, description, and display name, as well as the current status and startup type. Further, the PathName property identifies the process and arguments used to initiate the service, and the ProcessId property identifies the current process in memory if the service is in the running state.
Scheduled tasks are a bit more difficult to query. The Win32_ScheduledJob WMI class will allow you to query jobs that are created either by script or AT.exe, but not those created in the Scheduled Tasks control panel. You also have the option to use a whole set of classes in the TaskScheduler namespace if you want to get crazy. You can get a list of those classes using Get-WmiObject -List -Namespace Root/Microsoft/Windows/TaskScheduler. A simpler option is the Get-ScheduledTask cmdlet, which has a bunch of detail buried below the surface.
For starters, Get-ScheduledTask returns TaskName, TaskPath, and State without any digging. These properties give the friendly name you see in the Scheduled Tasks control panel, the path to the task in the navigation pane, and the status of the task.
Details like the application or command being executed require a bit more digging. The scheduled task stores this information under the Actions property in the Execute and Arguments properties. The following snippet queries the list of tasks that are not in the Disabled state, extracts the Action details and combines them into a new property named Command, and drops the output into 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 this is a great start, but what if you want to get some scheduling details. In the Scheduled Tasks control panel are Next Run Time and Last Run Time, which is perfect for summary purposes, but Get-ScheduledTask doesn’t provide these details directly. The good news is that we don’t have to dig too far to get runtime details, as the Get-ScheduledTaskInfo cmdlet gives us what we need.
To add the runtime details to our code snipped above we need to first get the task info details for each task as we loop through them with ForEach-Object. The easiest way to do this is to simply pipe the current task instance to the Get-ScheduledTaskInfo cmdlet using built in the $_ variable. Once we’ve done that, we’ll assign it to the $taskInfo variable, and then add the runtime 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 get to the key details for our tasks, let’s circle back to the initial goal of gathering data on tasks related to specific software tools.
There are a handful of properties that could contain details useful for correlating to 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. Both the -TaskName and -TaskPath parameters may be leveraged to query those properties and can even use wildcards to search for matches that aren’t exact.
The other bit of information that could be useful for searching is the Execute 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 this:
Get-ScheduledTask | Where-Object { $_.Actions.Execute -like ‘*Adobe*’ }
From there, the results can be piped to the rest of the code snippets we’ve developed above in order to fully develop the data set and produce a usable result.
Copyright © 2022 IDG Communications, Inc.