Shore Up
A clipboard walking down a row of identical desktop towers, noting a detail from each and adding a line to a long checklist.
Windows

Collect System Info from Remote PCs into a CSV with PowerShell

Ketan Aagja9 min read
No ratings yet

Before you run this

This guide builds a small PowerShell script that connects to a list of remote Windows PCs, reads their hardware and OS details (make, model, serial, CPU, RAM, disk free space, OS version, last boot), and writes one row per machine into a single CSV file. It is read-only. It queries WMI/CIM classes and creates no files, users, or registry keys on the remote machines, so there is nothing to undo on the targets — the only thing it writes is the CSV on your own machine.

Privileges: you need to run it as an account that is a local administrator on the remote PCs — a domain admin, or a delegated admin group added to local Administrators. Most inventory classes are readable by administrators only. Run the console as that account; you do not need an elevated (admin) PowerShell window on your own workstation unless local policy requires it, but running elevated does no harm.

Test first: run it against one test PC (put a single hostname in your list) and read the output before you point it at hundreds of machines. Confirm the CSV has the columns you expect. Because the script is read-only there is no destructive risk to the endpoints, but a typo in a property name will just give you blank columns, and you want to catch that on one row, not five hundred.

This is not a firewall change, so there is no config to back up — but if WinRM traffic crosses network segments, confirm with your network team that TCP 5985 (HTTP) is permitted before you blame the script for timeouts.

What I'm assuming

  • Your admin workstation runs Windows PowerShell 5.1 (ships with Windows 10/11 and Server 2016+). The script also runs unchanged on PowerShell 7.
  • Targets are domain-joined Windows PCs and you are querying from inside the domain.
  • WinRM is enabled on the targets. In most domains it is turned on by GPO. If not, Enable-PSRemoting -Force run once per target (or via GPO) is the standard way — see Microsoft Learn for Enable-PSRemoting and WS-Management (WinRM).
  • Name resolution works: the hostnames in your list resolve and are reachable.

The modern, mainstream tool for this is CIM (Get-CimInstance over a New-CimSession), which uses WinRM. The older Get-WmiObject/DCOM approach still exists but is deprecated; I use CIM here.

Step 1 — Confirm WinRM reaches a target

Before scripting anything, prove connectivity to one machine:

# Replace PC-TEST01 with a real hostname you can reach
Test-WSMan -ComputerName PC-TEST01

If that returns the WSMan identity block, WinRM is answering. If it errors, fix that first — the script below can't work until this does. Enable-PSRemoting on the target, or check the firewall/GPO, per the WinRM docs on Microsoft Learn.

Step 2 — Build your computer list

Put one hostname per line in a plain text file. Blank lines and anything you don't want are simply left out.

PC-TEST01
PC-TEST02
PC-TEST03

Save it as, for example, C:\path\to\computers.txt (substitute your own path).

Step 3 — The inventory script

Save this as Get-PCInventory.ps1. Read it before running it.

# --- Settings: change these two paths to yours ---
$ComputerList = "C:\path\to\computers.txt"     # one hostname per line
$OutputCsv    = "C:\path\to\inventory.csv"      # CSV to create/overwrite

# Read the list, ignoring blank lines
$computers = Get-Content -Path $ComputerList | Where-Object { $_.Trim() -ne "" }

$results = foreach ($name in $computers) {
    $name = $name.Trim()
    Write-Host "Querying $name ..."

    try {
        # One session per machine; DCOM fallback is deliberately not used
        $session = New-CimSession -ComputerName $name -ErrorAction Stop

        $os   = Get-CimInstance -CimSession $session -ClassName Win32_OperatingSystem
        $cs   = Get-CimInstance -CimSession $session -ClassName Win32_ComputerSystem
        $bios = Get-CimInstance -CimSession $session -ClassName Win32_BIOS
        $cpu  = Get-CimInstance -CimSession $session -ClassName Win32_Processor |
                    Select-Object -First 1
        # DriveType 3 = local fixed disk
        $sys  = Get-CimInstance -CimSession $session -ClassName Win32_LogicalDisk `
                    -Filter "DeviceID='C:'"

        # Build one flat record for the CSV
        [PSCustomObject]@{
            ComputerName   = $name
            Manufacturer   = $cs.Manufacturer
            Model          = $cs.Model
            SerialNumber   = $bios.SerialNumber
            CPU            = $cpu.Name
            Cores          = $cpu.NumberOfCores
            RAM_GB         = [math]::Round($cs.TotalPhysicalMemory / 1GB, 1)
            OS             = $os.Caption
            OSVersion      = $os.Version
            OSArchitecture = $os.OSArchitecture
            LastBootUp     = $os.LastBootUpTime
            C_Free_GB      = if ($sys) { [math]::Round($sys.FreeSpace / 1GB, 1) } else { $null }
            C_Size_GB      = if ($sys) { [math]::Round($sys.Size / 1GB, 1) } else { $null }
            LoggedOnUser   = $cs.UserName
            Status         = "OK"
        }

        Remove-CimSession -CimSession $session
    }
    catch {
        # Record the failure so it still gets a row in the CSV
        [PSCustomObject]@{
            ComputerName = $name
            Status       = "ERROR: $($_.Exception.Message)"
        }
    }
}

# NoTypeInformation keeps the old #TYPE header line out of the file
$results | Export-Csv -Path $OutputCsv -NoTypeInformation -Encoding UTF8

Write-Host "Done. Wrote $($results.Count) rows to $OutputCsv"

A few notes on the non-obvious parts:

  • New-CimSession uses WinRM by default. I create it explicitly so a bad session fails cleanly per-machine and one unreachable PC doesn't stop the run.
  • Win32_Processor returns one instance per socket; Select-Object -First 1 avoids duplicate CPU rows on multi-socket machines. If you need per-socket detail, that's a different report.
  • With Get-CimInstance, LastBootUpTime already comes back as a real DateTime. (The old Get-WmiObject path needed manual conversion — CIM does not.)
  • TotalPhysicalMemory, Size, and FreeSpace are in bytes; dividing by 1GB and rounding gives readable numbers.
  • Unreachable or unauthorised machines get a row with a Status of ERROR: and the reason, so you can see exactly which ones failed instead of silently missing them.

Step 4 — Run it

# Run as an account that is local admin on the targets
.\Get-PCInventory.ps1

You'll see one "Querying …" line per machine. Machines that don't answer within WinRM's timeout will surface as ERROR rows rather than hanging the whole run.

If you need to pass different credentials, New-CimSession accepts a -Credential parameter — see Microsoft Learn for New-CimSession for the exact syntax rather than guessing it here.

Step 5 — Verify it worked

Open the CSV and check three things:

Import-Csv "C:\path\to\inventory.csv" | Format-Table -AutoSize
  1. Row count matches your list. Every hostname should produce exactly one row.
  2. No unexpected blanks. If a whole column (say SerialNumber) is empty across all rows, you likely have a property-name mistake or the class isn't populated on that hardware — verify the property name against the WMI class reference on Microsoft Learn.
  3. Check the ERROR rows. Filter them to see what failed:
Import-Csv "C:\path\to\inventory.csv" | Where-Object { $_.Status -like "ERROR*" }

Common causes are WinRM not enabled, the firewall blocking 5985, name resolution, or your account not being local admin on that box. Fix and re-run just those hostnames by trimming your list.

Undo

There is nothing to roll back on the remote PCs — the script only reads. On your own machine the only artefact is the CSV; delete it if you don't want it. If you enabled WinRM on targets solely for this and want to reverse that, Disable-PSRemoting exists, but read its warnings on Microsoft Learn first: it does not fully undo everything Enable-PSRemoting set up, and in a managed domain you usually leave WinRM on.

Where to grow it

If you want this to run on a schedule, register it as a scheduled task under a service account with the right rights and append a datestamp to $OutputCsv. If you're pulling from Active Directory instead of a text file, Get-ADComputer (RSAT / ActiveDirectory module) can feed the list — but keep the query read-only and always filter to a known scope before you point it at the whole directory.

Written by
Ketan Aagja

Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.

More about the author →

Was this article helpful?

Tap a star — no sign-in needed.

Be the first to rate this article.

Audit Local Administrators Across Many Windows Machines

This is a read-only audit. It pulls a list of computers from Active Directory, connects to each one over PowerShell Remoting (WinRM), reads the membership of the local Administrators group, and writes everything to a single CSV you can open in Excel. It creates nothing and changes nothing on the target machines, so there is no destructive step and nothing to roll back — the only thing produced is the report file on your own workstation.

9 min read

Bulk-Update AD User Attributes from a CSV

This guide reads a CSV of users and writes two attributes back to Active Directory for each one: Title and Department . Nothing is created or deleted — existing user objects have those two fields overwritten with the values in your file. That overwrite is a real change: if the CSV has a wrong value in a row, that user's title or department is now wrong until you fix it. There is no built-in "undo," so the safety step below is to export the current values first so you can put them back.

9 min read

Export Installed Software Inventory From Many PCs With PowerShell

This guide builds a script that connects to a list of remote Windows machines over PowerShell remoting (WinRM), reads the "installed programs" data out of each machine's registry, and writes one combined CSV inventory. It is read-only : it queries registry keys and creates a CSV on your admin workstation. It does not install, uninstall, or change anything on the target machines.

8 min read

Generate a Disk-Space Report Across Servers with PowerShell Remoting

This guide builds a read-only disk-space report. It uses PowerShell remoting ( Invoke-Command over WinRM) to query each server's local fixed disks and returns size, free space, and percent free as one combined table you can export to CSV or HTML. It does not write to, resize, or clean up any disk — it only reads WMI/CIM data.

10 min read