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 generated ppc-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 without Invoke-Command timing out.
  • Invoke-Command -ComputerName $Computer -ScriptBlock { ... } -ErrorAction Stop: This is the core remoting mechanism.
    • It executes the provided ScriptBlock on the remote $Computer.
    • -ErrorAction Stop: Ensures that if Invoke-Command itself fails (e.g., WinRM not configured, access denied), the error is caught, and we can report it.
  • 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 a ComputerName property to each app object, identifying the source machine. $env:COMPUTERNAME refers to the remote computer’s name when executed via Invoke-Command.
  • $AllInstalledApps += $RemoteApps: Appends the collected applications from the current remote computer to our main $AllInstalledApps array.
  • Export-Csv -Path $AppListFile -NoTypeInformation -Encoding UTF8 -Force: After processing all computers, this command exports the entire $AllInstalledApps collection to app-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-Command will often work seamlessly.

  • Workgroup/Non-Domain Environments: In these scenarios, you’ll need to explicitly provide credentials. You can use the Get-Credential cmdlet to prompt for credentials and pass them to Invoke-Command using the -Credential parameter:

    $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-Credential is better for interactive use, or consider more secure options like SecretManagement modules 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:
    Enable-PSRemoting -Force
    
    This command configures WinRM for remote management, sets up appropriate firewall rules, and starts the WinRM service.
  • 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-PSRemoting usually 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 TrustedHosts on the initiating machine to allow connections to the target computers. This can be done with Set-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 -SessionOption with 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-Command with Multiple Computers: The Invoke-Command cmdlet 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 output
    

    This significantly speeds up the collection process. You can also adjust the throttle limit using -ThrottleLimit on Invoke-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-Job and Wait-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:
    # Find apps installed in the last 30 days
    $LastMonth = (Get-Date).AddDays(-30)
    $RecentApps = $AllInstalledApps | Where-Object { [datetime]::ParseExact($_.InstallDate, 'yyyyMMdd', $null) -ge $LastMonth }
    
    (Note: InstallDate from the registry is often in YYYYMMDD format, requiring parsing.)
  • Identify Duplicates/Conflicts: You could group by DisplayName and DisplayVersion to 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) or Win32_OperatingSystem (for OS info) can provide additional data. Get-CimInstance (using CIM, Common Information Model) is the modern PowerShell cmdlet for WMI queries.
    # Example using Get-CimInstance (modern WMI) to get OS details
    Get-CimInstance -ClassName Win32_OperatingSystem -ComputerName $Computer | Select-Object ComputerName, Caption, OSArchitecture
    
    However, for installed applications, the registry method often provides the most comprehensive and direct list that users see in “Programs and Features.”

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.