What This Script Does

This PowerShell script collects comprehensive hardware and software inventory from a Windows machine and uploads it to the FreeITSM Asset Inventory API. It requires no external tools — everything is gathered from WMI/CIM and the Windows registry. You can deploy it via Group Policy, SCCM, Intune, a scheduled task, or any other method you use to run scripts across your estate.

Data Collected

Hardware

  • Manufacturer & model
  • Service tag / serial number
  • CPU name & speed
  • Total physical memory
  • BIOS version
  • GPU name & VRAM

Operating System

  • OS name (e.g. Windows 11 Pro)
  • Feature release (e.g. 24H2)
  • Build number with UBR
  • Last boot time & uptime
  • Logged-in user
  • Domain / workgroup

Storage & Network

  • Logical drives with size & usage
  • Physical disk model & serial
  • Network adapters with IP, MAC
  • DHCP, gateway, DNS servers

Security & Software

  • TPM version & status
  • BitLocker encryption status
  • Full software inventory
  • Applications vs system components

Prerequisites

  • PowerShell: Version 5.1 or later (included with Windows 10/11 and Server 2016+)
  • API Key: Generate one in FreeITSM under System → Settings → API Keys
  • Administrator (optional): Run as admin for full TPM and BitLocker data

How It Works

  1. 1 Queries WMI/CIM for hardware, OS, disk, network, GPU, TPM, and BitLocker info
  2. 2 Reads the Windows registry to enumerate all installed software
  3. 3 Flags each application as user-visible or system component
  4. 4 Builds a JSON payload and sends it to the FreeITSM API
  5. 5 The API upserts the asset record, syncs disks, network adapters, and software

Usage

Download the script from the GitHub repository at scripts/Invoke-AssetInventory.ps1, then run it with your FreeITSM URL and API key.

# Post inventory directly to FreeITSM
.\Invoke-AssetInventory.ps1 -ApiUrl "https://itsm.yourcompany.com" -ApiKey "your-api-key"

# Save to a local file (useful for testing)
.\Invoke-AssetInventory.ps1 -OutputFile "C:\Temp\asset.json"

# Both: post to API and save locally
.\Invoke-AssetInventory.ps1 -ApiUrl "https://itsm.yourcompany.com" -ApiKey "your-api-key" -OutputFile "C:\Temp\asset.json"

The Script

The full script is shown below. You can also find it on GitHub.

<#
.SYNOPSIS
    Collects hardware, software, and system inventory from the local machine and
    posts it to FreeITSM.

.DESCRIPTION
    Gathers hostname, manufacturer, model, CPU, memory, OS, BIOS, disk, network,
    GPU, TPM, BitLocker, and installed software information then sends the data as
    a JSON payload to the FreeITSM asset inventory API.

    Run as Administrator for full results (BitLocker, TPM, and some disk details
    require elevation).

.PARAMETER ApiUrl
    The base URL of your FreeITSM instance (e.g. https://itsm.yourcompany.com).

.PARAMETER ApiKey
    API key for authentication.

.PARAMETER OutputFile
    Optional path to save the JSON output to a file instead of (or in addition to)
    posting to the API.

.EXAMPLE
    .\Invoke-AssetInventory.ps1 -ApiUrl "https://itsm.yourcompany.com" -ApiKey "abc123"

.EXAMPLE
    .\Invoke-AssetInventory.ps1 -OutputFile "C:\Temp\asset.json"
#>

[CmdletBinding()]
param(
    [string]$ApiUrl,
    [string]$ApiKey,
    [string]$OutputFile
)

# Require at least one output destination
if (-not $ApiUrl -and -not $OutputFile) {
    Write-Host ""
    Write-Host "FreeITSM Asset Inventory Collector" -ForegroundColor Cyan
    Write-Host "-----------------------------------" -ForegroundColor Cyan
    Write-Host ""
    Write-Host "Usage:" -ForegroundColor Yellow
    Write-Host "  .\Invoke-AssetInventory.ps1 -ApiUrl \"https://itsm.yourcompany.com\" -ApiKey \"your-key\""
    Write-Host "  .\Invoke-AssetInventory.ps1 -OutputFile \"C:\Temp\asset.json\""
    Write-Host ""
    Write-Host "Run as Administrator for full results (BitLocker, TPM)." -ForegroundColor Gray
    exit 1
}

$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

Write-Host "Collecting inventory data..." -ForegroundColor Cyan
if (-not $isAdmin) {
    Write-Host "  Note: Not running as Administrator. BitLocker and TPM data may be limited." -ForegroundColor Yellow
}

# ─── Core system info ───────────────────────────────────────────────────────────

$cs   = Get-CimInstance Win32_ComputerSystem
$os   = Get-CimInstance Win32_OperatingSystem
$bios = Get-CimInstance Win32_BIOS
$cpu  = Get-CimInstance Win32_Processor | Select-Object -First 1

# Feature release from registry (e.g. "23H2", "24H2")
$ntKey          = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$featureRelease = (Get-ItemProperty -Path $ntKey -Name DisplayVersion -ErrorAction SilentlyContinue).DisplayVersion
$ubr            = (Get-ItemProperty -Path $ntKey -Name UBR -ErrorAction SilentlyContinue).UBR
$buildNumber    = if ($ubr) { "$($os.BuildNumber).$ubr" } else { "$($os.BuildNumber)" }

Write-Host "  System info collected" -ForegroundColor Green

# ─── Disks ───────────────────────────────────────────────────────────────────────

$logicalDisks = @(Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
    @{
        drive        = $_.DeviceID
        label        = $_.VolumeName
        file_system  = $_.FileSystem
        size_bytes   = $_.Size
        free_bytes   = $_.FreeSpace
        used_percent = if ($_.Size -and $_.Size -gt 0) {
            [math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 1)
        } else { 0 }
    }
})

$physicalDisks = @(Get-CimInstance Win32_DiskDrive | ForEach-Object {
    @{
        model      = $_.Model
        serial     = if ($_.SerialNumber) { $_.SerialNumber.Trim() } else { $null }
        size_bytes = $_.Size
        media_type = $_.MediaType
        interface  = $_.InterfaceType
    }
})

Write-Host "  Disk info collected ($($logicalDisks.Count) logical, $($physicalDisks.Count) physical)" -ForegroundColor Green

# ─── Network adapters ────────────────────────────────────────────────────────────

$networkAdapters = @(Get-CimInstance Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" | ForEach-Object {
    @{
        name         = $_.Description
        mac_address  = $_.MACAddress
        ip_addresses = @($_.IPAddress)
        subnet_masks = @($_.IPSubnet)
        gateway      = @($_.DefaultIPGateway | Where-Object { $_ })
        dhcp_enabled = $_.DHCPEnabled
        dns_servers  = @($_.DNSServerSearchOrder | Where-Object { $_ })
    }
})

Write-Host "  Network info collected ($($networkAdapters.Count) adapters)" -ForegroundColor Green

# ─── GPU ─────────────────────────────────────────────────────────────────────────

$gpus = @(Get-CimInstance Win32_VideoController | ForEach-Object {
    @{
        name            = $_.Name
        driver_version  = $_.DriverVersion
        vram_bytes      = $_.AdapterRAM
        resolution      = "$($_.CurrentHorizontalResolution)x$($_.CurrentVerticalResolution)"
    }
})

Write-Host "  GPU info collected ($($gpus.Count) adapters)" -ForegroundColor Green

# ─── TPM ─────────────────────────────────────────────────────────────────────────

$tpm = $null
try {
    $tpmData = Get-CimInstance -Namespace "root\cimv2\Security\MicrosoftTpm" -ClassName Win32_Tpm -ErrorAction Stop
    if ($tpmData) {
        $tpm = @{
            version        = $tpmData.SpecVersion
            manufacturer   = $tpmData.ManufacturerIdTxt
            is_enabled     = $tpmData.IsEnabled_InitialValue
            is_activated   = $tpmData.IsActivated_InitialValue
        }
        Write-Host "  TPM info collected (v$($tpmData.SpecVersion))" -ForegroundColor Green
    }
} catch {
    Write-Host "  TPM info skipped (requires elevation or not present)" -ForegroundColor Gray
}

# ─── BitLocker ───────────────────────────────────────────────────────────────────

$bitlocker = @()
if ($isAdmin) {
    try {
        $bitlocker = @(Get-BitLockerVolume -ErrorAction Stop | ForEach-Object {
            @{
                drive             = $_.MountPoint
                protection_status = $_.ProtectionStatus.ToString()
                encryption_method = $_.EncryptionMethod.ToString()
                volume_status     = $_.VolumeStatus.ToString()
                lock_status       = $_.LockStatus.ToString()
            }
        })
        Write-Host "  BitLocker info collected ($($bitlocker.Count) volumes)" -ForegroundColor Green
    } catch {
        Write-Host "  BitLocker info skipped (not available)" -ForegroundColor Gray
    }
} else {
    Write-Host "  BitLocker info skipped (requires elevation)" -ForegroundColor Gray
}

# ─── Installed software ─────────────────────────────────────────────────────────

$regPaths = @(
    "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
)

$software = @(
    $regPaths | ForEach-Object {
        Get-ItemProperty -Path $_ -ErrorAction SilentlyContinue
    } | Where-Object {
        $_.DisplayName -and $_.DisplayName.Trim() -ne ''
    } | Sort-Object DisplayName -Unique | ForEach-Object {
        # SystemComponent=1 or ParentKeyName set = hidden from Add/Remove Programs
        $isComponent = ($_.SystemComponent -eq 1) -or ($_.ParentKeyName -and $_.ParentKeyName -ne '')

        @{
            display_name      = $_.DisplayName
            publisher         = $_.Publisher
            display_version   = $_.DisplayVersion
            install_date      = $_.InstallDate
            install_location  = $_.InstallLocation
            uninstall_string  = $_.UninstallString
            estimated_size    = if ($_.EstimatedSize) { "$($_.EstimatedSize)" } else { $null }
            system_component  = $isComponent
        }
    }
)

$appCount = ($software | Where-Object { -not $_.system_component }).Count
$componentCount = ($software | Where-Object { $_.system_component }).Count
Write-Host "  Software inventory collected ($appCount applications, $componentCount system components)" -ForegroundColor Green

# ─── Logged-in user ──────────────────────────────────────────────────────────────

$loggedInUser = $null
try {
    $explorerProc = Get-CimInstance Win32_Process -Filter "Name='explorer.exe'" -ErrorAction Stop | Select-Object -First 1
    if ($explorerProc) {
        $owner = Invoke-CimMethod -InputObject $explorerProc -MethodName GetOwner -ErrorAction Stop
        $loggedInUser = if ($owner.Domain) { "$($owner.Domain)\$($owner.User)" } else { $owner.User }
    }
} catch {
    $loggedInUser = $env:USERNAME
}

# ─── Last boot time ─────────────────────────────────────────────────────────────

$lastBoot = $os.LastBootUpTime.ToUniversalTime().ToString("yyyy-MM-dd HH:mm:ss")
$uptimeDays = [math]::Round(((Get-Date) - $os.LastBootUpTime).TotalDays, 1)

Write-Host "  Last boot: $lastBoot UTC (uptime: $uptimeDays days)" -ForegroundColor Green

# ─── Build the payload ───────────────────────────────────────────────────────────

$payload = [ordered]@{
    # Core asset fields (match assets table schema)
    hostname         = $env:COMPUTERNAME
    manufacturer     = $cs.Manufacturer
    model            = $cs.Model
    memory           = [long]$cs.TotalPhysicalMemory
    service_tag      = $bios.SerialNumber
    operating_system = $os.Caption -replace "Microsoft ", ""
    feature_release  = $featureRelease
    build_number     = $buildNumber
    cpu_name         = $cpu.Name
    speed            = [long]($cpu.MaxClockSpeed * 1000000)
    bios_version     = $bios.SMBIOSBIOSVersion
    domain           = $cs.Domain

    # Extended info
    logged_in_user   = $loggedInUser
    last_boot_utc    = $lastBoot
    uptime_days      = $uptimeDays

    # Disks
    disks = [ordered]@{
        logical  = $logicalDisks
        physical = $physicalDisks
    }

    # Network
    network_adapters = $networkAdapters

    # GPU
    gpus = $gpus

    # Security
    tpm              = $tpm
    bitlocker        = $bitlocker

    # Software inventory
    software         = $software
}

$json = $payload | ConvertTo-Json -Depth 5 -Compress:$false

# ─── Output ──────────────────────────────────────────────────────────────────────

if ($OutputFile) {
    $json | Out-File -FilePath $OutputFile -Encoding UTF8 -Force
    Write-Host ""
    Write-Host "JSON saved to: $OutputFile" -ForegroundColor Green
}

if ($ApiUrl) {
    $url = "$($ApiUrl.TrimEnd('/'))/api/external/system-info/submit/"
    $headers = @{ 'Content-Type' = 'application/json'; 'Authorization' = '' }
    if ($ApiKey) { $headers['Authorization'] = $ApiKey }

    Write-Host ""
    Write-Host "Posting to $url ..." -ForegroundColor Cyan

    try {
        $response = Invoke-RestMethod -Uri $url -Method POST -Headers $headers -Body ([System.Text.Encoding]::UTF8.GetBytes($json)) -ContentType 'application/json; charset=utf-8'
        Write-Host "Success!" -ForegroundColor Green
        Write-Host ($response | ConvertTo-Json -Compress) -ForegroundColor Gray
    } catch {
        Write-Host "Error posting to API: $_" -ForegroundColor Red
        if ($_.Exception.Response) {
            $reader = New-Object System.IO.StreamReader($_.Exception.Response.GetResponseStream())
            Write-Host "Response: $($reader.ReadToEnd())" -ForegroundColor Red
        }
        exit 1
    }
}

Write-Host ""
Write-Host "Done. Collected: $($software.Count) apps, $($logicalDisks.Count) drives, $($networkAdapters.Count) NICs, $($gpus.Count) GPUs" -ForegroundColor Cyan

Configuration

The script takes parameters directly — no need to edit the file. Just pass your FreeITSM URL and API key:

  • -ApiUrl – Your FreeITSM instance URL (e.g. https://itsm.yourcompany.com)
  • -ApiKey – The API key generated in System → Settings → API Keys
  • -OutputFile – Optional path to save the JSON locally for inspection

Deployment Tips

  • Scheduled Task: Run daily or weekly to keep inventory current. Use -ExecutionPolicy Bypass if needed
  • Group Policy: Deploy as a startup script across your domain for automatic enrolment
  • SCCM/Intune: Package as a script deployment for managed devices
  • Run as Admin: For full TPM and BitLocker data, ensure the script runs elevated
  • Manual: Run on individual machines as needed for ad-hoc inventory checks

← Back to Scripts