<# .SYNOPSIS Report installed BIOS version, date, and manufacturer; optionally flag firmware that is older than a configurable age threshold or that has a known newer version available from the vendor (Dell, HP, Lenovo). .DESCRIPTION Collects BIOS/UEFI metadata from WMI (Win32_BIOS + Win32_ComputerSystem) and reports it in a structured, human-readable block suitable for any RMM log viewer or interactive console. Vendor lookup (Dell / HP / Lenovo): Dell — parses the public CatalogPC.cab XML (no API key required). HP — parses the per-platform SoftPaq XML from ftp.hp.com using the 4-character product code from WMI (no API key required). Lenovo — parses download.lenovo.com/catalog/_Win.xml (no API key required). For any other vendor, or if the network lookup fails, the script falls back to the age-threshold check only (-MaxAgeDays). Exit codes (compatible with TRMM Script Checks and most other RMM platforms): 0 — BIOS is current (or no concern detected within thresholds) 1 — BIOS is outdated (older than MaxAgeDays, OR vendor reports newer version) 2 — Lookup failed / inconclusive (reported, but does not block 0 exit) .PARAMETER MaxAgeDays Flag the BIOS as potentially outdated if its release date is older than this many days. Default: 730 (2 years). Set to 0 to disable the age check. .PARAMETER SkipVendorLookup Skip the live vendor catalog lookup and rely solely on the age-threshold check. .PARAMETER TimeoutSec HTTP timeout in seconds for vendor catalog requests. Default: 15. .EXAMPLE .\Win_Config_BIOSVersionReport.ps1 .\Win_Config_BIOSVersionReport.ps1 -MaxAgeDays 365 .\Win_Config_BIOSVersionReport.ps1 -SkipVendorLookup .\Win_Config_BIOSVersionReport.ps1 -MaxAgeDays 0 -SkipVendorLookup .NOTES v1.0 2026-06-17 — Initial release. Vendor lookup: Dell CatalogPC.cab, HP SoftPaq XML, Lenovo download catalog XML. License: CC0 1.0 Universal (public domain). Requires: PowerShell 5.1+, run as Administrator/SYSTEM for reliable WMI access. #> [CmdletBinding()] param( [int] $MaxAgeDays = 730, [switch] $SkipVendorLookup, [int] $TimeoutSec = 15 ) #region ======================================================= HELPERS ======= function Format-Section { param([string]$Title) $line = '=' * 60 Write-Output '' Write-Output $line Write-Output " $Title" Write-Output $line } function Write-Result { param([string]$Label, [string]$Value) Write-Output (' {0,-22}: {1}' -f $Label, $Value) } # Safely parse a BIOS date string in CIM format (YYYYMMDD...) or ISO function ConvertTo-BIOSDate { param([string]$Raw) if (-not $Raw) { return $null } # WMI sometimes returns "20230415000000.000000+000" format if ($Raw -match '^(\d{4})(\d{2})(\d{2})') { try { return [datetime]::new([int]$Matches[1],[int]$Matches[2],[int]$Matches[3]) } catch { return $null } } try { return [datetime]::Parse($Raw) } catch { return $null } } #endregion #region ======================================================= WMI DATA ====== Format-Section 'BIOS / Firmware Inventory' $bios = Get-CimInstance -ClassName Win32_BIOS -ErrorAction SilentlyContinue $cs = Get-CimInstance -ClassName Win32_ComputerSystem -ErrorAction SilentlyContinue if (-not $bios) { Write-Output ' ERROR: Unable to query Win32_BIOS. Run as Administrator/SYSTEM.' exit 1 } $manufacturer = ($bios.Manufacturer -replace '\s+', ' ').Trim() $version = ($bios.SMBIOSBIOSVersion -replace '\s+', ' ').Trim() $serialNumber = ($bios.SerialNumber -replace '\s+', ' ').Trim() $model = if ($cs) { ($cs.Model -replace '\s+', ' ').Trim() } else { 'Unknown' } $biosDateRaw = $bios.ReleaseDate # Win32_BIOS.ReleaseDate is a CIM_DATETIME; CimInstance returns a [datetime] directly # on modern PS, but may return a string on PS 5.1 via Get-WmiObject. Handle both. if ($biosDateRaw -is [datetime]) { $biosDate = $biosDateRaw } else { $biosDate = ConvertTo-BIOSDate -Raw "$biosDateRaw" } $biosDateStr = if ($biosDate) { $biosDate.ToString('yyyy-MM-dd') } else { 'Unknown' } $biosAgeStr = if ($biosDate) { $days = ([datetime]::Today - $biosDate.Date).Days '{0} days ({1:F1} years)' -f $days, ($days / 365.25) } else { 'Unknown' } # Determine UEFI vs Legacy $fwType = try { $fi = Get-CimInstance -Namespace root\cimv2\Security\MicrosoftTPM ` -ClassName Win32_Tpm -ErrorAction SilentlyContinue # Use registry fallback — most reliable cross-version method $val = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\SecureBoot\State' ` -ErrorAction SilentlyContinue).UEFISecureBootEnabled if ($null -ne $val) { 'UEFI' } else { 'Legacy / Unknown' } } catch { 'Legacy / Unknown' } Write-Result 'Manufacturer' $manufacturer Write-Result 'Model' $model Write-Result 'BIOS Version' $version Write-Result 'BIOS Date' $biosDateStr Write-Result 'BIOS Age' $biosAgeStr Write-Result 'Serial Number' $serialNumber Write-Result 'Firmware Type' $fwType #endregion #region ======================================================= AGE CHECK ===== $ageFlag = $false if ($MaxAgeDays -gt 0) { Format-Section 'Age Threshold Check' if (-not $biosDate) { Write-Output " WARNING: BIOS date could not be parsed; skipping age check." } else { $ageDays = ([datetime]::Today - $biosDate.Date).Days if ($ageDays -gt $MaxAgeDays) { Write-Output (" FLAGGED : BIOS is {0} days old (threshold: {1} days)." ` -f $ageDays, $MaxAgeDays) $ageFlag = $true } else { Write-Output (" OK : BIOS is {0} days old (threshold: {1} days)." ` -f $ageDays, $MaxAgeDays) } } } #endregion #region ======================================================= VENDOR LOOKUP = $vendorFlag = $false $vendorLookupDone = $false $latestVersion = 'N/A' $latestDate = 'N/A' $lookupNote = '' if (-not $SkipVendorLookup) { Format-Section 'Vendor Catalog Lookup' # ---- Detect vendor ---- $vendorKey = switch -Wildcard ($manufacturer.ToUpper()) { '*DELL*' { 'Dell' } '*HP*' { 'HP' } '*HEWLETT*'{ 'HP' } '*LENOVO*' { 'Lenovo' } default { $null } } if (-not $vendorKey) { Write-Output (" INFO : Vendor '{0}' not supported for live lookup." ` -f $manufacturer) Write-Output ' Age-threshold check (above) is the only gate for this machine.' } else { Write-Output (" Vendor : {0}" -f $vendorKey) switch ($vendorKey) { #------------------------------------------------------------------ # DELL — public CatalogPC.cab (no auth) # URL : https://downloads.dell.com/catalog/CatalogPC.cab # The cab contains CatalogPC.xml; we parse BIOS components matched # by Win32_ComputerSystem.SystemSKUNumber (systemID attribute). #------------------------------------------------------------------ 'Dell' { try { $cabUrl = 'https://downloads.dell.com/catalog/CatalogPC.cab' $cabPath = Join-Path $env:TEMP 'DellCatalogPC.cab' $xmlPath = Join-Path $env:TEMP 'CatalogPC.xml' Write-Output ' Downloading Dell CatalogPC.cab (this may take 10-20 s)...' $wc = New-Object System.Net.WebClient $wc.Headers.Add('User-Agent', 'PowerShell/BIOSVersionReport') # Use timeout via WebRequest instead $wr = [System.Net.HttpWebRequest]::Create($cabUrl) $wr.Timeout = $TimeoutSec * 1000 $wr.UserAgent = 'PowerShell/BIOSVersionReport' $resp = $wr.GetResponse() $stream = $resp.GetResponseStream() $fs = [System.IO.File]::Create($cabPath) $stream.CopyTo($fs) $fs.Close(); $stream.Close(); $resp.Close() # Expand the CAB $null = & expand.exe $cabPath $xmlPath 2>&1 if (-not (Test-Path $xmlPath)) { throw "CAB expansion failed — CatalogPC.xml not found at $xmlPath" } # Load XML and find BIOS entries for this SKU # Win32_ComputerSystem.SystemSKUNumber on Dell == the 4-char hex SystemID $skuRaw = (Get-CimInstance Win32_ComputerSystem -ErrorAction SilentlyContinue).SystemSKUNumber # Some Dell WMI returns full model; the SKU is also in BIOS.SystemSKUNumber $skuAlt = (Get-CimInstance Win32_BIOS -ErrorAction SilentlyContinue).SystemSKUNumber [xml]$catalog = Get-Content $xmlPath -ErrorAction Stop $biosComponents = $catalog.Manifest.SoftwareComponent | Where-Object { $_.componentType.value -eq 'BIOS' -and ( ($_.SupportedSystems.Brand.Model.systemID -contains $skuRaw) -or ($_.SupportedSystems.Brand.Model.systemID -contains $skuAlt) ) } if (-not $biosComponents) { $lookupNote = "No BIOS entry found for SKU '$skuRaw' in Dell catalog." Write-Output (" WARNING : {0}" -f $lookupNote) } else { # Pick highest version (Dell uses dottedVersion attribute) $latest = $biosComponents | Sort-Object { try { [version]($_.dottedVersion -replace '[^0-9.]','') } catch { [version]'0.0' } } | Select-Object -Last 1 $latestVersion = $latest.dottedVersion $latestDate = $latest.dateTime $vendorLookupDone = $true Write-Result 'Latest BIOS (vendor)' $latestVersion Write-Result 'Latest BIOS date' $latestDate Write-Result 'Installed BIOS' $version # Simple string compare after stripping whitespace if ($latestVersion.Trim() -ne $version.Trim()) { Write-Output ' FLAGGED : Installed version differs from vendor latest.' $vendorFlag = $true } else { Write-Output ' OK : Installed version matches vendor latest.' } } # Cleanup Remove-Item $cabPath, $xmlPath -Force -ErrorAction SilentlyContinue } catch { $lookupNote = "Dell catalog lookup failed: $_" Write-Output (" WARNING : {0}" -f $lookupNote) } } #------------------------------------------------------------------ # HP — per-platform SoftPaq XML (no auth) # Platform ID (4-hex) comes from Win32_BaseBoard.Product # Catalog URL pattern: # https://ftp.hp.com/pub/hp/softlib/software13/spp/gm/swpackages/.xml # Fallback URL: # https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref//_64_Win11.cab #------------------------------------------------------------------ 'HP' { try { $bb = Get-CimInstance Win32_BaseBoard -ErrorAction SilentlyContinue $platformId = if ($bb.Product) { $bb.Product.Trim().ToLower() } else { $null } if (-not $platformId) { throw 'Could not determine HP Platform ID from Win32_BaseBoard.Product.' } Write-Output (" Platform ID : {0}" -f $platformId) # HP's per-platform advisory XML $xmlUrl = ('https://ftp.hp.com/pub/hp/softlib/software13/spp/gm/swpackages/{0}.xml' ` -f $platformId) $wr = [System.Net.HttpWebRequest]::Create($xmlUrl) $wr.Timeout = $TimeoutSec * 1000 $wr.UserAgent = 'PowerShell/BIOSVersionReport' $resp = $wr.GetResponse() $reader = New-Object System.IO.StreamReader($resp.GetResponseStream()) $xmlStr = $reader.ReadToEnd() $reader.Close(); $resp.Close() [xml]$catalog = $xmlStr # Filter to BIOS/Firmware category $biosSP = $catalog.SelectNodes('//Package') | Where-Object { $_.Category -match 'BIOS' -or $_.Category -match 'Firmware' } | Sort-Object { [version]($_.Version -replace '[^0-9.]','') } | Select-Object -Last 1 if (-not $biosSP) { $lookupNote = "No BIOS package found for platform '$platformId' in HP catalog." Write-Output (" WARNING : {0}" -f $lookupNote) } else { $latestVersion = $biosSP.Version $latestDate = $biosSP.DateReleased $vendorLookupDone = $true Write-Result 'Latest BIOS (vendor)' $latestVersion Write-Result 'Latest BIOS date' $latestDate Write-Result 'Installed BIOS' $version if ($latestVersion.Trim() -ne $version.Trim()) { Write-Output ' FLAGGED : Installed version differs from vendor latest.' $vendorFlag = $true } else { Write-Output ' OK : Installed version matches vendor latest.' } } } catch { $lookupNote = "HP catalog lookup failed: $_" Write-Output (" WARNING : {0}" -f $lookupNote) } } #------------------------------------------------------------------ # LENOVO — model-specific XML catalog (no auth) # MTM = first 4 chars of Win32_ComputerSystem.Model (e.g. "21F7") # URL = https://download.lenovo.com/catalog/_Win11.xml # (fall back to Win10 if Win11 returns 404) #------------------------------------------------------------------ 'Lenovo' { try { $mtm = $null if ($model -and $model.Length -ge 4) { $mtm = $model.Substring(0,4).Trim() } if (-not $mtm) { throw 'Could not determine Lenovo MTM from Win32_ComputerSystem.Model.' } Write-Output (" MTM Code : {0}" -f $mtm) $xmlContent = $null foreach ($winVer in @('Win11','Win10')) { $xmlUrl = ('https://download.lenovo.com/catalog/{0}_{1}.xml' -f $mtm, $winVer) try { $wr = [System.Net.HttpWebRequest]::Create($xmlUrl) $wr.Timeout = $TimeoutSec * 1000 $wr.UserAgent = 'PowerShell/BIOSVersionReport' $resp = $wr.GetResponse() $reader = New-Object System.IO.StreamReader($resp.GetResponseStream()) $xmlContent = $reader.ReadToEnd() $reader.Close(); $resp.Close() Write-Output (" Catalog : {0}" -f $xmlUrl) break } catch { <# try next winVer #> } } if (-not $xmlContent) { throw "No Lenovo catalog found for MTM '$mtm' (tried Win11 and Win10)." } [xml]$catalog = $xmlContent # Lenovo catalog: $biosPackages = $catalog.SelectNodes('//Package') | Where-Object { $_.category -match 'BIOS' -or $_.category -match 'UEFI' -or $_.id -match 'bios' } if (-not $biosPackages) { $lookupNote = "No BIOS package found for MTM '$mtm' in Lenovo catalog." Write-Output (" WARNING : {0}" -f $lookupNote) } else { # Sort by date, pick newest $latest = $biosPackages | Sort-Object { try { [datetime]$_.date } catch { [datetime]::MinValue } } | Select-Object -Last 1 $latestVersion = $latest.version $latestDate = $latest.date $vendorLookupDone = $true Write-Result 'Latest BIOS (vendor)' $latestVersion Write-Result 'Latest BIOS date' $latestDate Write-Result 'Installed BIOS' $version if ($latestVersion.Trim() -ne $version.Trim()) { Write-Output ' FLAGGED : Installed version differs from vendor latest.' $vendorFlag = $true } else { Write-Output ' OK : Installed version matches vendor latest.' } } } catch { $lookupNote = "Lenovo catalog lookup failed: $_" Write-Output (" WARNING : {0}" -f $lookupNote) } } } } } #endregion #region ======================================================= SUMMARY ======= Format-Section 'Summary' $exitCode = 0 $verdicts = @() if ($ageFlag) { $verdicts += ('BIOS age exceeds {0}-day threshold ({1}).' -f $MaxAgeDays, $biosAgeStr) $exitCode = 1 } if ($vendorFlag) { $verdicts += ('Vendor reports a newer BIOS version available: {0} (installed: {1}).' ` -f $latestVersion, $version) $exitCode = 1 } if ($lookupNote -and -not $vendorLookupDone -and -not $SkipVendorLookup) { $verdicts += "Vendor lookup inconclusive: $lookupNote" # Do NOT set exitCode=1 for a network failure alone; use exitCode 2 only # if no other flag triggered. if ($exitCode -eq 0) { $exitCode = 2 } } if ($exitCode -eq 0) { Write-Output ' RESULT : BIOS appears current. No action required.' } elseif ($exitCode -eq 1) { foreach ($v in $verdicts) { Write-Output (" ACTION : {0}" -f $v) } Write-Output '' Write-Output ' Recommendation: Update BIOS via vendor tool or update mechanism.' Write-Output (' Dell — Dell Command Update : https://www.dell.com/support/kbdoc/000177325') Write-Output (' HP — HP Image Assistant : https://ftp.hp.com/pub/caps-softpaq/cmit/HPIA.html') Write-Output (' Lenovo — System Update (LVSU) : https://support.lenovo.com/solutions/ht037099') } else { foreach ($v in $verdicts) { Write-Output (" INFO : {0}" -f $v) } Write-Output ' RESULT : Unable to confirm firmware status. Review manually.' } Write-Output '' Write-Output (' Exit Code: {0}' -f $exitCode) Write-Output '' exit $exitCode #endregion