In the dynamic world of IT administration, keeping an accurate inventory of software installed across all machines is not just a best practice—it’s a necessity. Whether you’re conducting a security audit, ensuring license compliance, troubleshooting application conflicts, or planning system upgrades, knowing exactly what’s running where is paramount. Manually checking each machine is impractical, especially in environments with dozens or hundreds of computers.
This guide provides a comprehensive, step-by-step approach to leverage PowerShell’s robust capabilities for remote system management. We’ll show you how to discover target machines within your network using DNS queries, remotely connect to them, extract a detailed list of all installed applications (including DisplayName, DisplayVersion, Publisher, and InstallDate) from both user and system contexts, and finally, compile this invaluable data into a single, easy-to-digest CSV report. By the end of this guide, you’ll have a powerful, automated solution to maintain an up-to-date software inventory across your Windows infrastructure.
1. Discovering Target Machines: Getting a PC List from DNS
Before we can query applications, we need a list of target computers. In a typical corporate environment, DNS (Domain Name System) is the authoritative source for hostnames and their corresponding IP addresses. We’ll use PowerShell to query DNS for machines that follow a specific naming convention—in our case, those starting with “PPC” (e.g., PPC1001, PPC1002).
This method is highly scalable and ensures you’re targeting active machines registered in your network.
Why DNS?
- Accuracy: DNS records are typically kept up-to-date by your domain controllers or DHCP servers.
- Scalability: You can easily query thousands of potential hostnames without hardcoding a list.
- Dynamic Environments: It accounts for machines coming online or offline without manual intervention.
Step-by-Step: Querying DNS for “PPC” Computers
We’ll use the Resolve-DnsName cmdlet, available in modern PowerShell versions, to find machines. This cmdlet is powerful and can query various DNS record types. For our purpose, we’re interested in A (Host) records.
First, let’s define our domain and the prefix for our computers. Replace "yourdomain.com" with your actual domain name.
# Define your domain name
$Domain = "yourdomain.com"
# Define the prefix for your target computers
$ComputerPrefix = "PPC"
# Define the output file for the PC list
$PCListFile = "ppc-list.txt"
Write-Host "Searching for computers starting with '$ComputerPrefix' in domain '$Domain'..." -ForegroundColor Cyan
try {
# Query all A records in the domain
# This might return a large number of records, so we'll filter them.
$DnsRecords = Resolve-DnsName -Name "*.$Domain" -Type A -ErrorAction Stop
# Filter for hostnames that start with our defined prefix
$TargetComputers = $DnsRecords |
Where-Object { $_.Type -eq "A" -and $_.Name -like "$ComputerPrefix*" } |
Select-Object -ExpandProperty Name |
Sort-Object -Unique
if ($TargetComputers.Count -gt 0) {
# Save the list of target computers to a text file
$TargetComputers | Out-File -FilePath $PCListFile -Encoding UTF8
Write-Host "Found $($TargetComputers.Count) computers. List saved to '$PCListFile'." -ForegroundColor Green
} else {
Write-Warning "No computers found matching the prefix '$ComputerPrefix' in domain '$Domain'."
}
catch {
Write-Error "An error occurred during DNS resolution: $($_.Exception.Message)"
Write-Warning "Please ensure your DNS servers are reachable and the domain name is correct."
}
```text
PPC1001
PPC1002
PPC1003
PPC1004
PPC1005
PPC1006
PPC1007
PPC1008
PPC1009
PPC1010
Example Output: Top 10 entries from ppc-list.txt
#### Code Explanation:
* **`$Domain` and `$ComputerPrefix`**: These variables make the script flexible. You can easily adapt it to different domains and naming conventions.
* **`Resolve-DnsName -Name "*.$Domain" -Type A`**: This is the core of our DNS query.
* `-Name "*.$Domain"`: We use a wildcard (`*`) to request all host records within the specified domain.
* `-Type A`: We specifically ask for A (Host) records, which map hostnames to IPv4 addresses.
* `-ErrorAction Stop`: If there's an issue with DNS resolution (e.g., domain not found, DNS server unreachable), the script will stop and report the error.
* **`Where-Object { $_.Type -eq "A" -and $_.Name -like "$ComputerPrefix*" }`**: This filters the potentially large list of DNS records. We ensure it's an A record and that the hostname (`$_.Name`) starts with our desired prefix (`PPC`).
* **`Select-Object -ExpandProperty Name`**: This extracts just the hostname string from each filtered DNS record object.
* **`Sort-Object -Unique`**: Ensures we have a sorted list of unique hostnames, removing any duplicates that might arise from DNS queries.
* **`Out-File -FilePath $PCListFile -Encoding UTF8`**: This saves our clean list of computer names to the specified text file (`ppc-list.txt`), using UTF8 encoding for broad compatibility.
Now that we have our `ppc-list.txt` populated with target machine names, the next step is to iterate through this list and query each machine for its installed applications.
---
## 2. Collecting Installed Applications: Querying Remote Registries
Installed applications on Windows are primarily registered in the system's registry. We need to query specific registry paths to retrieve details like the application's display name, version, publisher, and installation date. Since we're dealing with remote machines, we'll use PowerShell Remoting (`Invoke-Command`) to execute these registry queries on each target computer.
### Understanding Application Registry Paths
Applications can register themselves in different locations in the Windows Registry:
* **`HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\`**: This path typically contains applications installed for all users on the system (64-bit applications on a 64-bit OS).
* **`HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\`**: This path is for 32-bit applications installed on a 64-bit Windows operating system. The `Wow6432Node` indicates a redirection for 32-bit applications.
* **`HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\`**: This path contains applications installed specifically for the currently logged-on user. When querying remotely, this path is usually less relevant unless you're connecting with the specific user's credentials and trying to enumerate their user-specific installations. For a comprehensive inventory, focusing on HKLM paths is usually sufficient, as most enterprise software installs for all users. However, we'll include it for completeness, acknowledging its limitations in a remote, multi-user context.
### Step-by-Step: Retrieving Apps from Remote PCs
To achieve this, we'll read the list of computers from `ppc-list.txt` and loop through each one, using `Invoke-Command` to run a script block that queries the registry locally on the remote machine. The results will then be aggregated into a single collection.
```powershell
# Read the list of computers
$Computers = Get-Content -Path $PCListFile
# Define the output CSV file
$AppListFile = "app-list.csv"
# Create an empty array to store results from all computers
$AllInstalledApps = @()
Write-Host "Starting application inventory for $($Computers.Count) computers..." -ForegroundColor Cyan
foreach ($Computer in $Computers) {
Write-Host "Processing computer: $Computer" -ForegroundColor Yellow
try {
# Test connection to the computer first
# We use -ErrorAction SilentlyContinue and check the result to handle offline machines gracefully
$TestConnection = Test-Connection -ComputerName $Computer -Count 1 -ErrorAction SilentlyContinue -Quiet
if ($TestConnection) {
Write-Host "Successfully connected to $Computer. Querying installed applications..." -ForegroundColor Green
# Use Invoke-Command to run the registry query on the remote machine
$RemoteApps = Invoke-Command -ComputerName $Computer -ScriptBlock {
# Define registry paths to check for installed applications
$UninstallPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
# HKCU path is generally not useful for remote inventory as it's user-specific
# "HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
# Initialize an empty array for apps on this remote computer
$ComputerApps = @()
foreach ($Path in $UninstallPaths) {
Get-ItemProperty $Path -ErrorAction SilentlyContinue |
Where-Object {
# Filter out empty or unmanaged entries
$_.DisplayName -and ($_.SystemComponent -ne 1) -and ($_.ParentKeyName -eq $null)
} |
Select-Object @{Name="ComputerName"; Expression={$env:COMPUTERNAME}},
DisplayName,
DisplayVersion,
Publisher,
InstallDate |
ForEach-Object { $ComputerApps += $_ } # Add each app to the array
}
$ComputerApps # Return the collected apps
} -ErrorAction Stop
if ($RemoteApps) {
$AllInstalledApps += $RemoteApps
Write-Host "Found $($RemoteApps.Count) applications on $Computer." -ForegroundColor Green
} else {
Write-Warning "No applications found or retrieved from $Computer."
}
} else {
Write-Warning "Could not reach computer $Computer. It might be offline or unreachable."
}
}
catch {
Write-Error "An error occurred while processing $Computer: $($_.Exception.Message)"
Write-Warning "Ensure WinRM is enabled and configured on $Computer, and you have appropriate permissions."
}
}
# Export the aggregated list to a CSV file
if ($AllInstalledApps.Count -gt 0) {
$AllInstalledApps | Export-Csv -Path $AppListFile -NoTypeInformation -Encoding UTF8 -Force
Write-Host "Done! All installed applications saved to '$AppListFile'." -ForegroundColor Green
} else {
Write-Warning "No applications were collected from any of the target computers. '$AppListFile' was not created."
}
Code Explanation:
Get-Content -Path $PCListFile: Reads each line (computer name) from our previously generatedppc-list.txt.$AllInstalledApps = @(): Initializes an array to store application data from all remote computers. This allows us to collect all results before exporting them once.Test-Connection -ComputerName $Computer -Count 1 -ErrorAction SilentlyContinue -Quiet: Performs a quick ping test to check if the machine is online. This helps in gracefully handling offline machines withoutInvoke-Commandtiming out.Invoke-Command -ComputerName $Computer -ScriptBlock { ... } -ErrorAction Stop: This is the core remoting mechanism.- It executes the provided
ScriptBlockon the remote$Computer. -ErrorAction Stop: Ensures that ifInvoke-Commanditself fails (e.g., WinRM not configured, access denied), the error is caught, and we can report it.
- It executes the provided
- Registry Paths within
ScriptBlock: The script block defines the exact registry locations to query for installed software. - Filtering within
ScriptBlock(Where-Object):$_.DisplayName: Ensures we only process entries that have a display name.($_.SystemComponent -ne 1): Filters out system components that are not typically considered “installed applications” for inventory purposes.($_.ParentKeyName -eq $null): Helps filter out some redundant entries that are components of a larger application suite.
Select-Object @{Name="ComputerName"; Expression={$env:COMPUTERNAME}}, DisplayName, ...: Selects the relevant properties. Crucially, it adds aComputerNameproperty to each app object, identifying the source machine.$env:COMPUTERNAMErefers to the remote computer’s name when executed viaInvoke-Command.$AllInstalledApps += $RemoteApps: Appends the collected applications from the current remote computer to our main$AllInstalledAppsarray.Export-Csv -Path $AppListFile -NoTypeInformation -Encoding UTF8 -Force: After processing all computers, this command exports the entire$AllInstalledAppscollection toapp-list.csv, ensuring proper formatting and encoding.
3. Advanced Considerations and Best Practices
While the core script provides a functional solution, robust IT automation requires attention to details like permissions, error handling, and scalability. This section dives into critical considerations for deploying and maintaining your application inventory solution.
A. Permissions and Credentials for Remote Access
The Invoke-Command cmdlet operates under the security context of the user running the script on the initiating machine. For remote operations, this user typically needs administrative privileges on the target machines.
-
Domain Environments: If your PowerShell script is run from a domain-joined machine by a user with domain administrator rights (or rights specifically delegated for WinRM/remote management),
Invoke-Commandwill often work seamlessly. -
Workgroup/Non-Domain Environments: In these scenarios, you’ll need to explicitly provide credentials. You can use the
Get-Credentialcmdlet to prompt for credentials and pass them toInvoke-Commandusing the-Credentialparameter:$Credential = Get-Credential # ... then in your Invoke-Command call: # Invoke-Command -ComputerName $Computer -Credential $Credential -ScriptBlock { ... }Security Note: Avoid hardcoding credentials directly in scripts.
Get-Credentialis better for interactive use, or consider more secure options likeSecretManagementmodules or enterprise credential vaults for production automation.
B. PowerShell Remoting Setup (WinRM)
For Invoke-Command to work, PowerShell Remoting (WinRM) must be enabled and correctly configured on all target Windows machines.
- Enabling WinRM: On target machines, run the following in an elevated PowerShell prompt:
This command configures WinRM for remote management, sets up appropriate firewall rules, and starts the WinRM service.Enable-PSRemoting -Force - Firewall Considerations: Ensure that TCP port 5985 (for HTTP WinRM) or 5986 (for HTTPS WinRM) is open in the firewall between your initiating machine and the target computers.
Enable-PSRemotingusually handles this locally, but network firewalls might still block it. - Trusted Hosts: In workgroup environments or if your initiating machine is not in the same domain, you might need to configure
TrustedHostson the initiating machine to allow connections to the target computers. This can be done withSet-Item WSMan:\localhost\Client\TrustedHosts -Value "TargetComputer1, TargetComputer2".
C. Handling Offline or Unreachable Machines Gracefully
Our current script uses Test-Connection to pre-check connectivity. This is a good first step. However, machines can become unresponsive during the Invoke-Command call itself.
- Timeouts: For
Invoke-Command, you can specify a-SessionOptionwith a custom timeout. This prevents your script from hanging indefinitely on a problematic machine.$SessionOption = New-PSSessionOption -OperationTimeoutSeconds 60 # Timeout after 60 seconds # ... then in your Invoke-Command call: # Invoke-Command -ComputerName $Computer -SessionOption $SessionOption -ScriptBlock { ... } - Detailed Logging: Expanding on
Write-Host, implement more robust logging to a file. This allows you to review which machines succeeded, which failed, and why, without needing to watch the console output.
D. Scalability for Large Environments
Looping through hundreds or thousands of computers sequentially can be very slow. PowerShell offers ways to execute commands in parallel.
-
Invoke-Commandwith Multiple Computers: TheInvoke-Commandcmdlet can accept an array of computer names. When you pass multiple computers, PowerShell will automatically attempt to execute the script block on them concurrently (up to a default throttle limit of 32 concurrent operations).# Instead of a foreach loop, pass the entire array $AllComputers = Get-Content -Path $PCListFile $AllInstalledApps = Invoke-Command -ComputerName $AllComputers -ScriptBlock { # ... your existing script block to get apps from one computer ... } -ErrorAction SilentlyContinue | Select-Object ComputerName, DisplayName, DisplayVersion, Publisher, InstallDate # Select properties directly from the outputThis significantly speeds up the collection process. You can also adjust the throttle limit using
-ThrottleLimitonInvoke-Command. -
Background Jobs: For even more granular control over concurrency and to allow the script to continue running in the background, you could use
Start-JobandWait-Job.
E. Refining and Filtering Collected Data
The raw output of installed applications can be extensive. You’ll often want to filter or analyze this data further.
- Filter by Publisher:
$AdobeApps = $AllInstalledApps | Where-Object { $_.Publisher -like "*Adobe*" } - Filter by Installation Date:
(Note:
# Find apps installed in the last 30 days $LastMonth = (Get-Date).AddDays(-30) $RecentApps = $AllInstalledApps | Where-Object { [datetime]::ParseExact($_.InstallDate, 'yyyyMMdd', $null) -ge $LastMonth }InstallDatefrom the registry is often inYYYYMMDDformat, requiring parsing.) - Identify Duplicates/Conflicts: You could group by
DisplayNameandDisplayVersionto identify machines with different versions of the same software. - Missing Software: By comparing the collected inventory against a desired baseline, you can identify machines missing critical applications.
F. Alternative Methods: WMI and CIM
While querying the registry directly is effective, especially for the Uninstall keys, other methods exist for software inventory:
- WMI (Windows Management Instrumentation): WMI classes like
Win32_Product(though generally discouraged due to overhead) orWin32_OperatingSystem(for OS info) can provide additional data.Get-CimInstance(using CIM, Common Information Model) is the modern PowerShell cmdlet for WMI queries.However, for installed applications, the registry method often provides the most comprehensive and direct list that users see in “Programs and Features.”# Example using Get-CimInstance (modern WMI) to get OS details Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $Computer | Select-Object ComputerName, Caption, OSArchitecture
4. Full Script and Conclusion
Combining all the components, here is the complete script to discover “PPC” machines, collect their installed applications, and save the data to a CSV file.
# Define parameters
$Domain = "yourdomain.com" # !!! IMPORTANT: Replace with your actual domain !!!
$ComputerPrefix = "PPC"
$PCListFile = "ppc-list.txt"
$AppListFile = "app-list.csv"
$LogFile = "app-inventory.log" # For detailed logging
# --- Logging Function (Basic) ---
function Write-Log {
param (
[string]$Message,
[ValidateSet("INFO", "WARN", "ERROR")]$Level = "INFO"
)
$Timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
"$Timestamp [$Level] $Message" | Add-Content -Path $LogFile
Write-Host "$Timestamp [$Level] $Message"
}
# --- 1. Discovering Target Machines ---
Write-Log "Starting DNS discovery for computers starting with '$ComputerPrefix' in domain '$Domain'..."
try {
$DnsRecords = Resolve-DnsName -Name "*.$Domain" -Type A -ErrorAction Stop
$TargetComputers = $DnsRecords |
Where-Object { $_.Type -eq "A" -and $_.Name -like "$ComputerPrefix*" } |
Select-Object -ExpandProperty Name |
Sort-Object -Unique
if ($TargetComputers.Count -gt 0) {
$TargetComputers | Out-File -FilePath $PCListFile -Encoding UTF8
Write-Log "Found $($TargetComputers.Count) computers. List saved to '$PCListFile'." "INFO"
} else {
Write-Log "No computers found matching the prefix '$ComputerPrefix' in domain '$Domain'." "WARN"
}
}
catch {
Write-Log "An error occurred during DNS resolution: $($_.Exception.Message)" "ERROR"
Write-Log "Please ensure your DNS servers are reachable and the domain name is correct." "WARN"
exit # Exit if we can't get a computer list
}
# --- 2. Collecting Installed Applications ---
if (-not (Test-Path $PCListFile)) {
Write-Log "PC list file '$PCListFile' not found. Exiting application collection." "ERROR"
exit
}
$ComputersToProcess = Get-Content -Path $PCListFile
$AllInstalledApps = @()
$SessionOption = New-PSSessionOption -OperationTimeoutSeconds 120 # 2-minute timeout per machine
Write-Log "Starting application inventory for $($ComputersToProcess.Count) computers..."
foreach ($Computer in $ComputersToProcess) {
Write-Log "Processing computer: $Computer"
# Test-Connection first (optional, Invoke-Command will also fail if unreachable)
try {
$PingTest = Test-Connection -ComputerName $Computer -Count 1 -ErrorAction Stop -Quiet
if (-not $PingTest) {
Write-Log "Computer $Computer is not reachable via ping. Skipping." "WARN"
continue
}
}
catch {
Write-Log "Ping failed for $Computer: $($_.Exception.Message). Skipping." "WARN"
continue
}
try {
# Credential handling (uncomment and configure if needed for non-domain machines)
# $Credential = Get-Credential
$RemoteApps = Invoke-Command -ComputerName $Computer -SessionOption $SessionOption -ScriptBlock {
param($ComputerName) # Pass computer name as a parameter to the script block
$UninstallPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
$ComputerApps = @()
foreach ($Path in $UninstallPaths) {
Get-ItemProperty $Path -ErrorAction SilentlyContinue |
Where-Object {
$_.DisplayName -and ($_.SystemComponent -ne 1) -and ($_.ParentKeyName -eq $null)
} |
Select-Object @{Name="ComputerName"; Expression={$ComputerName}}, # Use the passed parameter
DisplayName,
DisplayVersion,
Publisher,
InstallDate |
ForEach-Object { $ComputerApps += $_ }
}
$ComputerApps
} -ArgumentList $Computer -ErrorAction Stop # Pass $Computer to the remote script block
if ($RemoteApps) {
$AllInstalledApps += $RemoteApps
Write-Log "Found $($RemoteApps.Count) applications on $Computer." "INFO"
} else {
Write-Log "No applications found or retrieved from $Computer." "WARN"
}
}
catch {
Write-Log "An error occurred while collecting apps from $Computer: $($_.Exception.Message)" "ERROR"
Write-Log "Ensure WinRM is enabled and configured, and you have appropriate permissions." "WARN"
}
}
# --- 3. Exporting Results ---
if ($AllInstalledApps.Count -gt 0) {
$AllInstalledApps | Export-Csv -Path $AppListFile -NoTypeInformation -Encoding UTF8 -Force
Write-Log "Done! All installed applications saved to '$AppListFile'." "INFO"
} else {
Write-Log "No applications were collected from any of the target computers. '$AppListFile' was not created." "WARN"
}
Write-Log "Application inventory script finished." "INFO"
"ComputerName","DisplayName","DisplayVersion","Publisher","InstallDate"
"PPC1001","Microsoft Edge","108.0.1462.46","Microsoft Corporation","20251201"
"PPC1001","Microsoft Visual C++ 2015-2019 Redistributable (x64)","14.29.30139.0","Microsoft Corporation","20251115"
"PPC1001","Notepad++","8.4.7","Don Ho","20251020"
"PPC1002","Google Chrome","108.0.5359.125","Google LLC","20251205"
"PPC1002","7-Zip 22.01 (x64)","22.01","Igor Pavlov","20251101"
"PPC1003","Microsoft Office Professional Plus 2021","16.0.15831.20204","Microsoft Corporation","20251210"
"PPC1003","VLC media player","3.0.18","VideoLAN","20250920"
"PPC1004","Zoom","5.12.9","Zoom Video Communications, Inc.","20251208"
"PPC1004","Adobe Acrobat Reader DC","22.003.20282","Adobe Systems Incorporated","20251125"
Example Output: Top 10 entries from app-list-20251225.csv
<!-- Screenshot Placeholder: A screenshot showing the content of the generated `app-list-20251225.csv` file opened in a spreadsheet program. -->
Conclusion
Maintaining an accurate and up-to-date software inventory is a non-trivial but essential task for system administrators. By leveraging PowerShell’s powerful remote management capabilities and a systematic approach to DNS discovery, registry querying, and robust error handling, you can automate this process efficiently. The script provided in this guide serves as a solid foundation, allowing you to regularly collect detailed application data from your Windows machines, empowering better decision-making for security, compliance, and asset management. Remember to adapt the domain and computer prefix to your environment, and always ensure proper WinRM configuration and permissions on your target machines for seamless operation. This automated inventory solution not only saves countless hours but also provides the critical insights needed to manage your IT estate effectively.