I'm a support analyst/help desk technician by trade. Here's a little PowerShell script I've cobbled together over the years to harvest details of Windows client and/or server platforms.
I use this script as part of my diagnostic process to streamline information gathering, minimize redundant communications, and enhance case note documentation.
It saves me a buttload of time by eliminating guesswork about system architecture and reducing unnecessary communications to obtain details I should have had in the first place.
The script extracts, formats, and writes the following data elements into a text file named ‘System Inventory.txt’ saved in the logged-on user’s Downloads folder:
- OS Details
- OEM Serial Number
- GPU specs
- Storage Capacity
- Optional Windows Features or Server Roles (contingent upon platform)
- ISP Details & External IP Addresses (both IPv4 & IPv6)
- Antivirus details
- PowerShell & .NET Framework versions
- Browser URL associations - 'What did you say your default browser was?'
- Critical & Error events over the past 24 hours
- Updates & Hotfixes
So, how exactly do I use the script? Real basic:
- Share the script with the client
- Walk them through running the script
- Retrieve the results (if remotely assisting) or have the client send the System Inventory.txt file to me via email
- Post the client's system inventory as an internal case note in whatever ITSM\CRM platform (SalesForce, ServiceNow, Remedy, etc) my shop is using
- Diagnose, reproduce, and fix the client's issue
I'm very interested in knowing the community's thoughts. Any constructive criticism would be greatly appreciated.
# System Inventory Script Overview
# Ownership:
# This script is the property of William John (Bill) Hamill. Unauthorized copying, distribution, or use of this script is prohibited.
# Synopsis:
# Collects detailed system inventory information, including GPU properties, storage capacity, Windows features, Server Roles, network adapters,
# ISP details, antivirus details, PowerShell & .NET Framework versions, browser URL associations, critical/error events, and updates/hotfixes.
# Designed to be shared as 'System Inventory.txt' and run in PowerShell ISE using the instructions below.
# Functional within PowerShell or VS Code Console when saved as a .ps1 file (e.g., System Inventory.ps1).
# Instructions:
# 1) Download this text file | Open with Notepad.
# 2) Search for Windows PowerShell ISE | Right-click (Run as Administrator).
# Note: This script *must* be run as an administrator to work!
# 3) PowerShell ISE | View | Show Script Pane
# 4) Notepad | Control + A to select all text, then Control + C to copy it.
# 5) PowerShell ISE | Script Pane | Control + V to paste the copied text from Notepad.
# 6) PowerShell ISE | File | Run. The results will be displayed on the screen and saved to your Downloads folder as "System Inventory - <ComputerName>.txt".
# Disclaimer:
# This script is provided "as is," without warranty of any kind. Use at your own risk. The author or distributor shall not be held liable
# for any damage or issues arising from the use of this script.
# Define Error Handling, Line Breaks, Initialize Output File & Progress Bar
$ErrorActionPreference = "Stop"; $NewLine = "`n"; $SystemInventory = @(); $Global:TaskCount = 0; $Global:TotalTasks = 13
# Check elevation status, halt if not running as admin.
function Confirm-ElevationStatus {
if (-not ([Security.Principal.WindowsPrincipal]::new([Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltinRole]::Administrator))) {
Clear-Host; Write-Host "This script must be run as an administrator to work!" -ForegroundColor Red; Write-Host
exit 1
}
}
# Bypass execution policy for the current PowerShell session only. Security will be restored to its previous state once this session is closed.
function Disable-ExecutionPolicy {
$ExecutionContext.InvokeCommand.InvokeScript('Set-ExecutionPolicy Bypass -Scope Process')
}
# Hide the script pane in PowerShell ISE by simulating 'Ctrl + R' key press and clear the console screen
function Hide-ScriptPane {
if ($host.Name -eq 'Windows PowerShell ISE Host') {
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.SendKeys]::SendWait("^r")
Clear-Host
}
}
# Graphics Card Details
function Get-GpuProperties {
try {
$gpuProperties = Get-CimInstance -ClassName Win32_VideoController
$gpuDetails = foreach ($gpu in $gpuProperties) {
[PSCustomObject]@{
'Device Name' = $gpu.Name
'Video RAM' = [Math]::Round($gpu.AdapterRAM / 1MB, 2)
'Driver Version' = $gpu.DriverVersion
'Install Date & Time' = $gpu.DriverDate
}
}
return $gpuDetails
}
catch {
throw "An error occurred while retrieving GPU properties: $($_.Exception.Message)"
}
}
# Storage Capacity
function Get-FreeSpace {
try {
Get-CimInstance -Class Win32_LogicalDisk |
Select-Object SystemName, DeviceID, VolumeName,
@{ Name = "Free Space (GB)"; expression = { "{0:N2}" -f ($_.Freespace / 1GB) } },
@{ Name = "Total Size (GB)"; expression = { "{0:N2}" -f ($_.Size / 1GB) } },
@{ Name = "Free Space %" ; expression = { "{0:N2}" -f (($_.Freespace / $_.Size) * 100) } }
}
catch {
throw "An error occurred retrieving free space information: $($_.Exception.Message)"
}
}
# Optional Windows Features\Active Server Roles
function Get-RolesAndFeatures {
try {
$CimSysType = (Get-CimInstance Win32_OperatingSystem).ProductType
if ($CimSysType -eq 1) {
$FeatureLabel = "# Windows Client - Active Optional Features"
$RolesAndFeatures = Get-WindowsOptionalFeature -Online | Where-Object State -EQ 'Enabled' | Select-Object FeatureName | Sort-Object FeatureName
}
else {
$FeatureLabel = "# Windows Server - Installed Server Roles"
$RolesAndFeatures = Get-WindowsFeature | Where-Object InstallState -EQ 'Installed' | Select-Object DisplayName
}
return [PSCustomObject]@{
FeatureLabel = $FeatureLabel
RolesAndFeatures = $RolesAndFeatures
}
}
catch {
throw "An error occurred retrieving roles and features: $($_.Exception.Message)"
}
}
# .Net Properties
function Get-DotNetProperties {
# Source: https://stackoverflow.com/questions/3487265/powershell-script-to-return-versions-of-net-framework-on-a-machine
try {
# .NET Framework Lookup Table
$Lookup = @{
378389 = '4.5'
378675 = '4.5.1'
378758 = '4.5.1'
379893 = '4.5.2'
393295 = '4.6'
393297 = '4.6'
394254 = '4.6.1'
394271 = '4.6.1'
394802 = '4.6.2'
394806 = '4.6.2'
460798 = '4.7'
460805 = '4.7'
461308 = '4.7.1'
461310 = '4.7.1'
461808 = '4.7.2'
461814 = '4.7.2'
528040 = '4.8'
528049 = '4.8'
533320 = '4.8.1'
}
# Retrieve .NET Framework Versions
$netFrameworkVersions = Get-ChildItem 'HKLM:\SOFTWARE\Microsoft\NET Framework Setup\NDP' -Recurse |
Get-ItemProperty -Name Version, Release -ErrorAction SilentlyContinue |
Where-Object { $_.PSChildName -eq "Full" } |
Select-Object @{ Name = "Client" ; Expression = { $_.PSChildName } },
@{ Name = "Version"; Expression = { $Lookup[$_.Release] } },
Release
# Retrieve .NET Core Versions
$dotNetCoreVersions = & "C:\Program Files\dotnet\dotnet.exe" --list-runtimes |
ForEach-Object {
$parts = $_ -split '\s+'
[PSCustomObject]@{
'Client' = $parts[0]
'Version' = $parts[1]
'Path' = $parts[2]
}
}
# Combine Results
$combinedResults = @(); $combinedResults += $netFrameworkVersions; $combinedResults += $dotNetCoreVersions
if ($combinedResults) { return $combinedResults } else { Write-Output "No .NET versions found on this machine." }
}
catch {
throw "An error occurred while enumerating .NET properties: $($_.Exception.Message)"
}
}
# ISP Details & External IP Addresses
function Get-ISPDetails {
try {
$IPv4 = Invoke-RestMethod -Uri 'http://ipinfo.io/'
$IPv6 = Invoke-RestMethod -Uri 'http://ident.me/'
return [PSCustomObject]@{
IPv4 = $IPv4
IPv6 = $IPv6
}
}
catch {
throw "An error occurred while retrieving ISP details: $($_.Exception.Message)"
}
}
# Antivirus Details
function Get-AntiVirus {
# Source: https://jdhitsolutions.com/blog/powershell/5187/get-antivirus-product-status-with-powershell/
try {
$antivirus = Get-CimInstance -Namespace "root\SecurityCenter2" -ClassName "AntiVirusProduct"
}
catch {
throw "An error occurred while retrieving antivirus information: $($_.Exception.Message)"
return
}
# Decode hexadecimal productState value to derive Enabled & UpToDate values (True or False)
Function ConvertTo-Hex {
Param([int]$Number)
try {
return '0x{0:x}' -f $Number
}
catch {
throw "Failed to convert number to hexadecimal: $($_.Exception.Message)"
return $null
}
}
$AntiVirusDetails = $antivirus | ForEach-Object {
try {
$hx = ConvertTo-Hex $_.ProductState
if ($hx -and $hx.Length -ge 5) {
$mid = $hx.Substring(3, 2)
$Enabled = if ($mid -match "00|01") { $False } else { $True }
$end = $hx.Substring(5)
$UpToDate = if ($end -eq "00") { $True } else { $False }
}
else {
$Enabled = $False
$UpToDate = $False
}
# Collect and format results
[PSCustomObject]@{
"Display Name" = $_.displayName
"Install Path" = $_.pathToSignedReportingExe
"Enabled" = $Enabled
"Updated" = $UpToDate
"Latest Scan" = Get-Date $_.timestamp -Format "dddd, dd-MMM-yyyy hh:mm:ss tt"
"Computer" = $Env:COMPUTERNAME
}
}
catch {
throw "Failed to process antivirus product: $($_.Exception.Message)"
}
}
$AntiVirusDetails
}
# Browser URL Associations
function Get-BrowserURL {
try {
$AppHash = @{}
$RegPath = "HKCU:\Software\Microsoft\Windows\Shell\Associations\UrlAssociations"
Get-ChildItem "$RegPath\*\UserChoice\" -ErrorAction SilentlyContinue |
ForEach-Object {
$AppHash.Add((Get-Item $_.PSParentPath).PSChildName, $_.GetValue('progId'))
}
if ( $AppHash.Count -gt 0 ) { $AppHash.GetEnumerator() | Sort-Object Value, Name }
}
catch {
throw "An error occurred while retrieving browser URL associations: $($_.Exception.Message)"
}
}
# Event Log Activity
function Get-CriticalErrorEvents {
try {
$startTime = (Get-Date).AddDays(-1)
$events = Get-WinEvent -FilterHashtable @{
LogName = 'System', 'Application'
Level = 1, 2
StartTime = $startTime
}
$events | Select-Object LevelDisplayName, Id, TimeCreated, ProviderName, Message | Sort-Object LevelDisplayName, ID
}
catch {
if ($_.Exception.Message -match "No events were found that match the specified selection criteria") {
Write-Output $NewLine
Write-Output "No Critical or Error events raised over the past 24 hours."
Write-Output $NewLine
}
else {
throw "An error occurred while retrieving the event logs: $($_.Exception.Message)"
}
}
}
# Updates & Hotfixes
function Get-WindowsUpdateHistory {
try {
$Session = New-Object -ComObject Microsoft.Update.Session
$Search = $Session.CreateUpdateSearcher()
$Count = $Search.GetTotalHistoryCount()
$Patch = $Search.QueryHistory(0, $Count)
$Updates = New-Object System.Collections.ArrayList
foreach ($Update in $Patch) {
if ($Update.Operation -eq 1 -and $Update.ResultCode -eq 2 -and $Update.Title -notlike '*KB2267602*') {
# Daily virus definition updates filtered for brevity: KB2267602 Security Intelligence Update for Microsoft Defender Antivirus
$Updates.Add([PSCustomObject]@{
'KB Number' = [regex]::match($Update.Title, 'KB(\d+)').Value
'Installed' = $Update.Date
'Title' = $Update.Title
'Description' = $Update.Description
}) | Out-Null
}
}
$Updates
}
catch {
throw "An error occurred while retrieving Windows update history: $($_.Exception.Message)"
}
}
# Progress Bar Helper Function
function Update-Progress {
param (
[string]$Activity,
[string]$Status
)
try {
$global:TaskCount++
Write-Progress -Activity $Activity -Status $Status -PercentComplete (($global:TaskCount / $global:TotalTasks) * 100)
}
catch {
throw "An error occurred while updating progress: $($_.Exception.Message)"
}
}
# Verify elevation, configure environment
Confirm-ElevationStatus; Disable-ExecutionPolicy; Hide-ScriptPane
# Current Date
$TimeStamp = Get-Date -Format F
$SystemInventory = "# Current Date", $NewLine, $TimeStamp, $NewLine | Out-String
# Detailed System Information
Update-Progress -Activity "Gathering system inventory" -Status "Detailed Operating System Information" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$OS = systeminfo.exe
$SystemInventory += "# Operating System", $OS, $NewLine | Out-String
# OEM Identifier
Update-Progress -Activity "Gathering system inventory" -Status "OEM Identifier\Serial Number\Service Tag" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$OemUniqueId = Get-CimInstance -ClassName Win32_Bios | Select-Object SerialNumber
$SystemInventory += "# OEM Identifier", $OemUniqueId | Out-String
# Graphics Card Details
Update-Progress -Activity "Gathering system inventory" -Status "Graphics Card Details" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$GraphicCards = Get-GpuProperties | Sort-Object 'Video Ram' -Descending
$SystemInventory += "# Graphic Cards", $GraphicCards | Out-String
# Storage Capacity
Update-Progress -Activity "Gathering system inventory" -Status "Storage Capacity" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$Storage = Get-FreeSpace | Format-Table -AutoSize
$SystemInventory += "# Storage Capacity", $Storage | Out-String
# Server Roles\Optional Windows Features
Update-Progress -Activity "Gathering system inventory" -Status "Server Roles\Optional Windows Features" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$Results = Get-RolesAndFeatures
$SystemInventory += $Results.FeatureLabel, $Results.RolesAndFeatures | Out-String
# Network Adapters & Mac Addresses
Update-Progress -Activity "Gathering system inventory" -Status "Network Adapters & Mac Addresses" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$NetAdapters = Get-NetAdapter | Format-Table -AutoSize
$SystemInventory += "# Network Adapters & Mac Addresses", $NetAdapters | Out-String
# ISP Details & External IP Addresses
Update-Progress -Activity "Gathering system inventory" -Status "ISP Details & External IP Addresses" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$ISPDetails = Get-ISPDetails
$SystemInventory += "# ISP Details & External IP Addresses", $ISPDetails.IPv4, $ISPDetails.IPv6 | Out-String
# Security Posture - Antivirus\Antimalware Details
Update-Progress -Activity "Gathering system inventory" -Status "Security Posture - Antivirus\Antimalware Details" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$AntiVirus = Get-AntiVirus
$SystemInventory += "# Antivirus Details", $AntiVirus | Out-String
# PowerShell
Update-Progress -Activity "Gathering system inventory" -Status "PowerShell Details" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$PoSh = $PSVersionTable.PSVersion
$SystemInventory += "# PowerShell", $PoSh | Out-String
# .Net Properties
Update-Progress -Activity "Gathering system inventory" -Status ".NET Properties" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$NetFrmWrk = Get-DotNetProperties
$SystemInventory += "# .Net Framework", $NetFrmWrk | Out-String
# Browswer URL Associations
Update-Progress -Activity "Gathering system inventory" -Status "Browswer URL Associations" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$BrowserURLs = Get-BrowserURL
$SystemInventory += "# Browser URL Associations", $BrowserURLs | Out-String
# Event Log Activity
Update-Progress -Activity "Gathering system inventory" -Status "Event Log Activity" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$EventLogActivty = Get-CriticalErrorEvents | Format-Table -AutoSize -Wrap
$SystemInventory += "# Event Log Activity", $EventLogActivty | Out-String
# Updates & Hotfixes
Update-Progress -Activity "Gathering system inventory" -Status "Updates & Hotfixes" -PercentComplete (($TaskCount / $TotalTasks) * 100)
$UpdateHistory = Get-WindowsUpdateHistory | Select-Object "KB Number", Installed, Title | Sort-Object Installed -Descending
$SystemInventory += "# Updates & Hotfixes", $UpdateHistory | Out-String
# Save & Display Results
Clear-Host
$FileName = Join-Path $Env:USERPROFILE "Downloads\System Inventory - $($Env:COMPUTERNAME).txt"
$SystemInventory | Out-File -FilePath $FileName -Encoding ascii
Get-Content -Path $FileName
Write-Host "Hard copy saved as" $FileName; Write-Host
1 Answer 1
There are a few things that stand out for me ...
- look into switching to Posh7 instead of Posh5
the 1st has been greatly improved over the 2nd.
- take a look at how scripts & functions are generally laid out.
Get-Help about_Comment_Based_Help
Get-Help about_Scripts
- consider using the '#Requires" statement
Get-Help about_Requires
this would let you require running as admin, and/or require running a particular version of Posh, and/or require a module without having to manually test for it.
- look into code folding
the #region & #endregion comments let you fold a block out of the way while you work on other stuff. it's nice to fold up a function you are done with while working on the newer stuff.
- look at shorter lines of code
you have several that go quite a ways off to the right. you can line-wrap at many "natural" places ... like after a pipe, an operator, or a comma. avoid backticks if at all possible.
you have a section after # Define Error Handling that puts several commands on one long line. that can be difficult to read ... and easy to miss the part that is off to the far right.
a bit of useful reading on shorter lines, the line continuation idea, and the problems with backticks ...
Get-PowerShellBlog:
Bye Bye Backtick:
Natural Line Continuations in PowerShell
— https://get-powershellblog.blogspot.com/2017/07/bye-bye-backtick-natural-line.html
- look into organizing parameters via "splatting"
Get-Help about_Splatting
look at something other than '+=' for adding to a collection it's known to be somewhat slow on Posh5, but has been mostly fixed in Posh7+.
look into something other than simple arrays
they are FIXED in size, so when you add to them you are actually making a new one. the [ArrayList] used to be the recommended structure, but there are others that are newer/better.
arrays - For PowerShell ArrayList, which is Faster .Add or += Operator? - Stack Overflow
— https://stackoverflow.com/questions/54208763/for-powershell-arraylist-which-is-faster-add-or-operator
one good reason to avoid arraylists is that when you .add() to an arraylist, it outputs an index that needs to be consumed.
take a look at the Generic.List for a better way ...
[System.Collections.Generic.List[object]]::new()
=====
Your code seems functional to me. I tried something similar for a friend several years ago ... this link uses my old ID ...
SystemInfo - v-4 - Pastebin.com
— https://pastebin.com/cUPynbNp