GuíasGuides · Nivel 2Level 2

PowerShell: un script de inventario que corre en remoto, maneja errores y deja un CSVPowerShell: an inventory script that runs remotely, handles errors and leaves a CSV

Objetos en lugar de texto, Invoke-Command sobre una lista de equipos, try/catch para los que no responden, y un CSV que abre Excel. Con la política de ejecución y la firma explicadas.Objects instead of text, Invoke-Command over a list of devices, try/catch for the ones that don't answer, and a CSV Excel can open. With execution policy and signing explained.

Qué vas a conseguir

Un script que, dada una lista de equipos, se conecta a cada uno, saca modelo, número de serie, versión de Windows, disco libre y último arranque, y deja un CSV con una fila por equipo, incluidos los que no respondieron y por qué. Es el patrón que repetirás en la mitad de los scripts de IT.

Antes de empezar

  • PowerShell 5.1 (viene con Windows) o PowerShell 7 (winget install Microsoft.PowerShell). El script vale en los dos.
  • WinRM activado en los equipos de destino (en dominio suele estarlo por GPO; si no, Enable-PSRemoting -Force en cada uno, o la GPO *Permitir la administración remota del servidor a través de WinRM*).
  • Tu cuenta con administrador local en los equipos, o una credencial para pasarla con -Credential.
  • Un editor con PowerShell integrado: VS Code con la extensión de PowerShell marca errores mientras escribes.

1. Objetos, no texto

La diferencia con cmd y con Bash: un comando de PowerShell devuelve objetos con propiedades, no líneas. Por eso puedes seleccionar, filtrar y exportar sin cortar cadenas.

PowerShell
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, LastBootUpTimeCaption                          Version    LastBootUpTime-------                          -------    --------------Microsoft Windows 11 Enterprise  10.0.26100 03/10/2026 8:12:40(Get-CimInstance Win32_OperatingSystem).LastBootUpTime -lt (Get-Date).AddDays(-14)True

Get-CimInstance sustituye a Get-WmiObject (obsoleto). Get-Member sobre cualquier objeto te enseña qué propiedades tiene.

2. El script

VS Code · Get-DeviceInventory.ps1
#Requires -Version 5.1<#.SYNOPSIS  Inventario básico de equipos por WinRM, con un CSV de salida..EXAMPLE   .\Get-DeviceInventory.ps1 -ComputerName (Get-Content equipos.txt) -OutFile inventario.csv#>[CmdletBinding()]param(    [Parameter(Mandatory)] [string[]] $ComputerName,    [string] $OutFile = "inventario-$(Get-Date -Format yyyyMMdd).csv",    [pscredential] $Credential) $remote = {    $os  = Get-CimInstance Win32_OperatingSystem    $cs  = Get-CimInstance Win32_ComputerSystem    $bio = Get-CimInstance Win32_BIOS    $c   = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"    [pscustomobject]@{        Computer   = $env:COMPUTERNAME        Model      = "$($cs.Manufacturer) $($cs.Model)"        Serial     = $bio.SerialNumber        OS         = $os.Caption        Build      = $os.Version        FreeGB     = [math]::Round($c.FreeSpace / 1GB, 1)        LastBoot   = $os.LastBootUpTime        LastUser   = $cs.UserName    }} $results = foreach ($pc in $ComputerName) {    Write-Verbose "Consultando $pc"    try {        $p = @{ ComputerName = $pc; ScriptBlock = $remote; ErrorAction = 'Stop' }        if ($Credential) { $p.Credential = $Credential }        $r = Invoke-Command @p        $r | Select-Object * -ExcludeProperty PSComputerName, RunspaceId, PSShowComputerName |             Add-Member -NotePropertyName Status -NotePropertyValue OK -PassThru    }    catch {        [pscustomobject]@{ Computer = $pc; Model = $null; Serial = $null; OS = $null; Build = $null                           FreeGB = $null; LastBoot = $null; LastUser = $null                           Status = "ERROR: $($_.Exception.Message -replace '\s+',' ')" }    }} $results | Export-Csv -Path $OutFile -NoTypeInformation -Encoding UTF8 -Delimiter ';'$ok = ($results | Where-Object Status -eq 'OK').CountWrite-Host "$ok de $($ComputerName.Count) equipos respondieron. CSV: $OutFile"

Lo que hace que sea un script y no un apaño: param con tipos (así -ComputerName acepta una lista), [CmdletBinding()] que da -Verbose gratis, el bloque remoto que devuelve un objeto por equipo, try/catch con ErrorAction Stop para que un equipo apagado no pare el bucle, y el CSV con ; como separador para que Excel en español lo abra en columnas.

3. Ejecutarlo

PowerShell
.\Get-DeviceInventory.ps1 -ComputerName LAPTOP-7F2K9Q, DESK-MAD-201, LAPTOP-OLD-1 -VerboseVERBOSE: Consultando LAPTOP-7F2K9QVERBOSE: Consultando DESK-MAD-201VERBOSE: Consultando LAPTOP-OLD-12 de 3 equipos respondieron. CSV: inventario-20261021.csvImport-Csv .\inventario-20261021.csv -Delimiter ';' | Format-Table Computer, FreeGB, LastBoot, StatusComputer       FreeGB LastBoot            Status--------       ------ --------            ------LAPTOP-7F2K9Q    21,4 03/10/2026 8:12:40  OKDESK-MAD-201    188,0 20/10/2026 7:55:02  OKLAPTOP-OLD-1                              ERROR: Connecting to remote server LAPTOP-OLD-1 failed with the following error message : WinRM cannot complete the operation...

Para cientos de equipos, Invoke-Command acepta la lista entera a la vez y los consulta en paralelo (-ThrottleLimit 32); el bucle de arriba es más lento pero deja claro qué equipo falló y por qué.

4. Política de ejecución y firma

Si al ejecutarlo aparece *"no se puede cargar porque la ejecución de scripts está deshabilitada"*, es la política de ejecución. No es seguridad contra atacantes; es un seguro contra ejecutar scripts por accidente.

PowerShell
Get-ExecutionPolicy -List        Scope ExecutionPolicy        ----- ---------------  MachinePolicy       Undefined     UserPolicy       Undefined        Process       Undefined    CurrentUser       Undefined   LocalMachine      RestrictedSet-ExecutionPolicy -Scope CurrentUser RemoteSigned

RemoteSigned ejecuta lo que escribes tú y exige firma a lo descargado. Si la empresa la fija por GPO en AllSigned, los scripts del equipo se firman con un certificado de firma de código de la CA interna (Set-AuthenticodeSignature). Es lo correcto en producción y encaja con el repositorio de la guía de Git: solo lo revisado se firma.

Siguientes pasos

  • Cambia la lista de equipos por Get-ADComputer -Filter {OperatingSystem -like "Windows 11*"} y tendrás el inventario de todo el dominio.
  • Pasa el resultado por PSScriptAnalyzer (Invoke-ScriptAnalyzer .\Get-DeviceInventory.ps1) antes de subirlo al repo.
  • En el Nivel 3 verás Microsoft Graph desde PowerShell: el mismo patrón, contra Entra ID e Intune en lugar de WinRM.

¿Falta algo o en tu entorno es distinto? Escríbeme y lo corrijo.

What you'll get

A script that, given a list of devices, connects to each one, pulls model, serial number, Windows version, free disk and last boot, and leaves a CSV with one row per device, including the ones that didn't answer and why. It is the pattern you'll repeat in half of all IT scripts.

Before you start

  • PowerShell 5.1 (ships with Windows) or PowerShell 7 (winget install Microsoft.PowerShell). The script works on both.
  • WinRM enabled on the target devices (in a domain it usually is, by GPO; if not, Enable-PSRemoting -Force on each, or the GPO *Allow remote server management through WinRM*).
  • Your account as local administrator on the devices, or a credential to pass with -Credential.
  • An editor with PowerShell support: VS Code with the PowerShell extension flags errors as you type.

1. Objects, not text

The difference from cmd and Bash: a PowerShell command returns objects with properties, not lines. That is why you can select, filter and export without slicing strings.

PowerShell
Get-CimInstance Win32_OperatingSystem | Select-Object Caption, Version, LastBootUpTimeCaption                          Version    LastBootUpTime-------                          -------    --------------Microsoft Windows 11 Enterprise  10.0.26100 03/10/2026 8:12:40(Get-CimInstance Win32_OperatingSystem).LastBootUpTime -lt (Get-Date).AddDays(-14)True

Get-CimInstance replaces Get-WmiObject (deprecated). Get-Member on any object shows you which properties it has.

2. The script

VS Code · Get-DeviceInventory.ps1
#Requires -Version 5.1<#.SYNOPSIS  Basic device inventory over WinRM, with a CSV output..EXAMPLE   .\Get-DeviceInventory.ps1 -ComputerName (Get-Content devices.txt) -OutFile inventory.csv#>[CmdletBinding()]param(    [Parameter(Mandatory)] [string[]] $ComputerName,    [string] $OutFile = "inventory-$(Get-Date -Format yyyyMMdd).csv",    [pscredential] $Credential) $remote = {    $os  = Get-CimInstance Win32_OperatingSystem    $cs  = Get-CimInstance Win32_ComputerSystem    $bio = Get-CimInstance Win32_BIOS    $c   = Get-CimInstance Win32_LogicalDisk -Filter "DeviceID='C:'"    [pscustomobject]@{        Computer   = $env:COMPUTERNAME        Model      = "$($cs.Manufacturer) $($cs.Model)"        Serial     = $bio.SerialNumber        OS         = $os.Caption        Build      = $os.Version        FreeGB     = [math]::Round($c.FreeSpace / 1GB, 1)        LastBoot   = $os.LastBootUpTime        LastUser   = $cs.UserName    }} $results = foreach ($pc in $ComputerName) {    Write-Verbose "Querying $pc"    try {        $p = @{ ComputerName = $pc; ScriptBlock = $remote; ErrorAction = 'Stop' }        if ($Credential) { $p.Credential = $Credential }        $r = Invoke-Command @p        $r | Select-Object * -ExcludeProperty PSComputerName, RunspaceId, PSShowComputerName |             Add-Member -NotePropertyName Status -NotePropertyValue OK -PassThru    }    catch {        [pscustomobject]@{ Computer = $pc; Model = $null; Serial = $null; OS = $null; Build = $null                           FreeGB = $null; LastBoot = $null; LastUser = $null                           Status = "ERROR: $($_.Exception.Message -replace '\s+',' ')" }    }} $results | Export-Csv -Path $OutFile -NoTypeInformation -Encoding UTF8$ok = ($results | Where-Object Status -eq 'OK').CountWrite-Host "$ok of $($ComputerName.Count) devices answered. CSV: $OutFile"

What makes it a script rather than a hack: param with types (so -ComputerName takes a list), [CmdletBinding()] which gives you -Verbose for free, the remote block returning one object per device, try/catch with ErrorAction Stop so a switched-off device doesn't stop the loop, and a UTF-8 CSV Excel opens straight away (add -Delimiter ';' if your Excel locale expects semicolons).

3. Run it

PowerShell
.\Get-DeviceInventory.ps1 -ComputerName LAPTOP-7F2K9Q, DESK-MAD-201, LAPTOP-OLD-1 -VerboseVERBOSE: Querying LAPTOP-7F2K9QVERBOSE: Querying DESK-MAD-201VERBOSE: Querying LAPTOP-OLD-12 of 3 devices answered. CSV: inventory-20261021.csvImport-Csv .\inventory-20261021.csv | Format-Table Computer, FreeGB, LastBoot, StatusComputer       FreeGB LastBoot            Status--------       ------ --------            ------LAPTOP-7F2K9Q    21.4 03/10/2026 8:12:40  OKDESK-MAD-201    188.0 20/10/2026 7:55:02  OKLAPTOP-OLD-1                              ERROR: Connecting to remote server LAPTOP-OLD-1 failed with the following error message : WinRM cannot complete the operation...

For hundreds of devices, Invoke-Command takes the whole list at once and queries them in parallel (-ThrottleLimit 32); the loop above is slower but makes it obvious which device failed and why.

4. Execution policy and signing

If running it shows *"cannot be loaded because running scripts is disabled"*, that is the execution policy. It is not security against attackers; it is a guard against running scripts by accident.

PowerShell
Get-ExecutionPolicy -List        Scope ExecutionPolicy        ----- ---------------  MachinePolicy       Undefined     UserPolicy       Undefined        Process       Undefined    CurrentUser       Undefined   LocalMachine      RestrictedSet-ExecutionPolicy -Scope CurrentUser RemoteSigned

RemoteSigned runs what you write and requires a signature on anything downloaded. If the company pins it to AllSigned by GPO, the team's scripts are signed with a code-signing certificate from the internal CA (Set-AuthenticodeSignature). That is the right thing in production and fits the repository from the Git guide: only what is reviewed gets signed.

Next steps

  • Swap the device list for Get-ADComputer -Filter {OperatingSystem -like "Windows 11*"} and you have the inventory of the whole domain.
  • Run the result through PSScriptAnalyzer (Invoke-ScriptAnalyzer .\Get-DeviceInventory.ps1) before pushing it to the repo.
  • In Level 3 you'll see Microsoft Graph from PowerShell: the same pattern, against Entra ID and Intune instead of WinRM.

Is something missing, or different in your environment? Get in touch and I'll fix it.

Tech Career Lab · newsletter

¿Te ha servido? Cada dos semanas envío una idea práctica para perfiles IT: LinkedIn, proyectos, networking y procesos de selección.Found this useful? Every two weeks I send one practical idea for IT professionals: LinkedIn, projects, networking and hiring processes.

Apúntate y llévate las 33 ideasSign up and get the 33 ideas →