modules/SystemInventory/SystemInventory.psm1
|
<# SystemInventory.psm1 Ultimate Modular Windows Server Discovery Toolkit - System Inventory collector. Owns datasets: ExecutionContext, OperatingSystem, Hardware, BiosFirmware, Processors, Memory, PageFile, PendingReboot, PhysicalVirtualDetection, HardwareVendor. (TimeSync is owned by the dedicated TimeSync module - see modules\TimeSync.) Design rules (enforced throughout this module): - Windows PowerShell 5.1 compatible. No PS7-only syntax. - STRICTLY READ-ONLY. Only CIM/registry reads, read-only cmdlets, and read-only command-line tools (w32tm /query). Nothing changes the system. - Defensive: every collection block is wrapped in try/catch so a single failure never stops the module. No Set-StrictMode. - Emits the canonical dataset field names the RiskEngine rules depend on (OperatingSystem.IsEndOfLifeOrNear, ExecutionContext.IsAdmin, PendingReboot.RebootPending) as real booleans. #> #region Private helpers ------------------------------------------------------- function Get-SIFirst { <# Returns the first element of a possibly-null/empty array, or $null. #> param($Value) if ($null -eq $Value) { return $null } $arr = @($Value) if ($arr.Count -gt 0) { return $arr[0] } return $null } function ConvertTo-SIBool { <# Coerces a CIM/registry value to a real [bool], with a default for $null. #> param($Value, [bool]$Default = $false) if ($null -eq $Value) { return $Default } try { return [bool]$Value } catch { return $Default } } function Get-SIString { <# Trims a value to a clean string; $null becomes ''. #> param($Value) if ($null -eq $Value) { return '' } try { return ([string]$Value).Trim() } catch { return '' } } function Get-SIDatasetRow { <# Returns the first row of a named dataset, or $null. #> param([object]$Context, [string]$Name) try { if ($Context -and $Context.DataSets -and $Context.DataSets.Contains($Name)) { $rows = @($Context.DataSets[$Name].Rows) if ($rows.Count -gt 0) { return $rows[0] } } } catch { } return $null } function Get-SIChassisType { <# Maps a Win32_SystemEnclosure ChassisTypes code to a friendly label. #> param($Codes) $map = @{ 1='Other';2='Unknown';3='Desktop';4='Low Profile Desktop';5='Pizza Box'; 6='Mini Tower';7='Tower';8='Portable';9='Laptop';10='Notebook';11='Hand Held'; 12='Docking Station';13='All in One';14='Sub Notebook';15='Space-Saving'; 16='Lunch Box';17='Main System Chassis';18='Expansion Chassis';19='SubChassis'; 20='Bus Expansion Chassis';21='Peripheral Chassis';22='Storage Chassis'; 23='Rack Mount Chassis';24='Sealed-Case PC';28='Blade';29='Blade Enclosure'; 32='Tablet';35='Convertible';36='Detachable' } $first = Get-SIFirst $Codes if ($null -eq $first) { return '' } $key = 0 if ([int]::TryParse([string]$first, [ref]$key) -and $map.ContainsKey($key)) { return $map[$key] } return ("Code {0}" -f $first) } function Get-SIOsEdition { <# Best-effort friendly OS edition from Caption keyword, then SKU fallback. #> param([string]$Caption, $Sku) $cap = Get-SIString $Caption foreach ($w in @('Datacenter','Enterprise','Essentials','Foundation','Standard','Web','Education','Professional','Ultimate','Business','Home','Pro')) { if ($cap -match ('(?i)\b' + [regex]::Escape($w) + '\b')) { return $w } } $skuMap = @{ 7='Standard Server';8='Datacenter Server';10='Enterprise Server'; 12='Datacenter Server Core';13='Standard Server Core';14='Enterprise Server Core'; 4='Enterprise';27='Enterprise (N)';48='Professional';49='Professional (N)'; 101='Home';161='Pro for Workstations';175='Enterprise for Virtual Desktops' } if ($null -ne $Sku) { $skuInt = 0 if ([int]::TryParse([string]$Sku, [ref]$skuInt) -and $skuMap.ContainsKey($skuInt)) { return $skuMap[$skuInt] } } return '' } function Get-SIEndOfLifeInfo { <# Conservative end-of-life / near-end-of-life determination from OS caption. Returns @{ IsEol=[bool]; Known=[bool]; Evidence=[string] }. If the OS cannot be classified, Known=$false and IsEol=$false (caller records an Unknown rather than guessing). #> param([string]$Caption, [string]$BuildNumber) $cap = Get-SIString $Caption $result = @{ IsEol = $false; Known = $true; Evidence = '' } if ([string]::IsNullOrWhiteSpace($cap)) { $result.Known = $false; return $result } $isServer = ($cap -match '(?i)server') if ($isServer) { if ($cap -match '(?i)(2000|2003|2008|2012)') { $result.IsEol = $true $result.Evidence = ("'{0}' is a Windows Server release (2003/2008/2008 R2/2012/2012 R2 or older) that is past Microsoft end of support." -f $cap) } elseif ($cap -match '(?i)2016') { $result.IsEol = $true $result.Evidence = ("'{0}' (Windows Server 2016) extended support ends 2027-01-12 - within the near-end-of-life window." -f $cap) } elseif ($cap -match '(?i)(2019|2022|2025)') { $result.IsEol = $false $result.Evidence = ("'{0}' is a currently supported Windows Server release." -f $cap) } else { $result.Known = $false $result.Evidence = ("Could not confidently classify support status of server OS '{0}'." -f $cap) } } else { if ($cap -match '(?i)(Windows XP|Windows Vista|Windows 7|Windows 8)') { $result.IsEol = $true $result.Evidence = ("'{0}' is a client Windows release (XP/Vista/7/8/8.1) that is past Microsoft end of support." -f $cap) } elseif ($cap -match '(?i)Windows 10') { $result.IsEol = $true $result.Evidence = ("'{0}' (Windows 10) reached end of support on 2025-10-14 (excluding paid ESU)." -f $cap) } elseif ($cap -match '(?i)Windows 11') { $result.IsEol = $false $result.Evidence = ("'{0}' (Windows 11) is currently supported." -f $cap) } else { $result.Known = $false $result.Evidence = ("Could not confidently classify support status of client OS '{0}'." -f $cap) } } return $result } function Get-SIPlatformDetection { <# Infers physical vs. virtual platform from manufacturer/model/BIOS strings and the SMBIOS asset tag. Returns @{ Platform; Evidence; Confidence }. #> param([string]$Manufacturer, [string]$Model, [string]$BiosManufacturer, [string]$BiosVersion, [string]$AssetTag) $mfg = Get-SIString $Manufacturer $model = Get-SIString $Model $bMfg = Get-SIString $BiosManufacturer $bVer = Get-SIString $BiosVersion $tag = Get-SIString $AssetTag $hay = ("{0} | {1} | {2} | {3}" -f $mfg, $model, $bMfg, $bVer) $evidence = ("Manufacturer='{0}'; Model='{1}'; BIOS='{2} {3}'" -f $mfg, $model, $bMfg, $bVer) # Azure uses a well-known SMBIOS chassis asset tag. if ($tag -eq '7783-7084-3265-9085-8269-3286-77') { return @{ Platform='Azure (Hyper-V)'; Evidence=("Azure well-known SMBIOS asset tag detected. {0}" -f $evidence); Confidence='Confirmed' } } if ($hay -match '(?i)VMware') { return @{ Platform='VMware'; Evidence=$evidence; Confidence='Confirmed' } } if ($hay -match '(?i)VirtualBox|innotek') { return @{ Platform='VirtualBox'; Evidence=$evidence; Confidence='Confirmed' } } if ($hay -match '(?i)QEMU|KVM') { return @{ Platform='KVM/QEMU'; Evidence=$evidence; Confidence='Confirmed' } } if ($hay -match '(?i)Xen') { return @{ Platform='Xen'; Evidence=$evidence; Confidence='Confirmed' } } if ($hay -match '(?i)Parallels') { return @{ Platform='Parallels'; Evidence=$evidence; Confidence='Confirmed' } } if ($hay -match '(?i)Amazon|EC2') { return @{ Platform='AWS EC2'; Evidence=$evidence; Confidence='Confirmed' } } if ($hay -match '(?i)Google') { return @{ Platform='Google Compute Engine'; Evidence=$evidence; Confidence='Confirmed' } } if ($mfg -match '(?i)Microsoft' -and $model -match '(?i)Virtual Machine') { return @{ Platform='Hyper-V'; Evidence=("Microsoft 'Virtual Machine' model detected. {0}" -f $evidence); Confidence='Likely' } } if ($hay -match '(?i)Virtual') { return @{ Platform='Virtual (unspecified)'; Evidence=("Generic virtual marker detected. {0}" -f $evidence); Confidence='Possible' } } return @{ Platform='Physical'; Evidence=("No virtualization markers detected. {0}" -f $evidence); Confidence='Likely' } } #endregion #region Contract: metadata & prerequisites ------------------------------------ function Get-DiscoveryModuleMetadata { [pscustomobject]@{ ModuleName = 'SystemInventory' DisplayName = 'System Inventory' Category = 'System' Version = '1.0.0' DefaultInFast = $true DefaultInDeep = $true RequiresAdmin = $false RequiresDomainContext = $false RequiresRole = $null EstimatedImpact = 'Low' CanRunAsSystem = $true ProducesDatasets = @('ExecutionContext','OperatingSystem','Hardware','BiosFirmware','Processors','Memory','PageFile','PendingReboot','PhysicalVirtualDetection','HardwareVendor') ProducesRisks = $true ProducesFollowUpQuestions = $true SupportsDeepMode = $true SupportsComplianceLens = $false } } function Test-DiscoveryPrerequisites { param([object]$Context) # System inventory relies on core CIM/registry which are always present. # It runs (with reduced completeness) even without elevation, so it is always Ready. $limitations = @() if ($Context -and -not $Context.IsAdmin) { $limitations += 'Not elevated: Secure Boot / TPM / some firmware details may be unavailable.' } [pscustomobject]@{ ModuleName = 'SystemInventory' CanRun = $true Status = 'Ready' Reason = '' Limitations = @($limitations) } } #endregion #region Contract: collection -------------------------------------------------- function Invoke-DiscoveryCollection { param([object]$Context) Write-SectionStatus -Title 'System Inventory' -Status 'Collecting' -Context $Context $raw = @{ ExecutionContext = $null OperatingSystem = $null Hardware = $null BiosFirmware = $null Processors = @() Memory = @() PageFile = @() PendingReboot = $null PhysicalVirtualDetection = $null HardwareVendor = $null } # --- Shared CIM reads (each never throws; returns @()) -------------------- $cs = Get-SIFirst (Invoke-CimSafe -ClassName 'Win32_ComputerSystem') $os = Get-SIFirst (Invoke-CimSafe -ClassName 'Win32_OperatingSystem') $bios = Get-SIFirst (Invoke-CimSafe -ClassName 'Win32_BIOS') $enclosure = Get-SIFirst (Invoke-CimSafe -ClassName 'Win32_SystemEnclosure') $baseboard = Get-SIFirst (Invoke-CimSafe -ClassName 'Win32_BaseBoard') $procs = @(Invoke-CimSafe -ClassName 'Win32_Processor') $mem = @(Invoke-CimSafe -ClassName 'Win32_PhysicalMemory') $pagefiles = @(Invoke-CimSafe -ClassName 'Win32_PageFileUsage') if (-not $os) { Add-Limitation -Context $Context -Module 'SystemInventory' -Message 'Win32_OperatingSystem was unavailable; OS details may be incomplete.' -Impact 'Reduced OS detail' | Out-Null } if (-not $cs) { Add-Limitation -Context $Context -Module 'SystemInventory' -Message 'Win32_ComputerSystem was unavailable; hardware details may be incomplete.' -Impact 'Reduced hardware detail' | Out-Null } # --- ExecutionContext ----------------------------------------------------- try { $currentUser = '' try { $currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } catch { $currentUser = ("{0}\{1}" -f $env:USERDOMAIN, $env:USERNAME) } $curDir = '' try { $curDir = (Get-Location).Path } catch { $curDir = '' } $culture = '' try { $culture = (Get-Culture).Name } catch { $culture = '' } $tzName = '' try { if (Get-CommandAvailable -Name 'Get-TimeZone') { $tzName = (Get-TimeZone).DisplayName } else { $tzName = Get-SIString (Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\TimeZoneInformation' -Name 'TimeZoneKeyName') } } catch { $tzName = '' } $raw.ExecutionContext = [pscustomobject]([ordered]@{ IsAdmin = (ConvertTo-SIBool $Context.IsAdmin) IsSystem = (ConvertTo-SIBool $Context.IsSystem) PowerShellVersion = (Get-SIString $Context.PowerShellVersion) CurrentUser = $currentUser CurrentDirectory = $curDir Culture = $culture TimeZone = $tzName RunId = (Get-SIString $Context.RunId) Mode = (Get-SIString $Context.Mode) ProjectType = (Get-SIString $Context.ProjectType) ComputerName = (Get-SIString $Context.ComputerName) }) } catch { Write-Log -Level WARN -Message 'ExecutionContext collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- OperatingSystem ------------------------------------------------------ try { if ($os) { $caption = Get-SIString $os.Caption $build = Get-SIString $os.BuildNumber $eol = Get-SIEndOfLifeInfo -Caption $caption -BuildNumber $build $raw.OperatingSystem = [pscustomobject]([ordered]@{ Caption = $caption Version = (Get-SIString $os.Version) BuildNumber = $build Edition = (Get-SIOsEdition -Caption $caption -Sku $os.OperatingSystemSKU) OperatingSystemSKU = (Get-SIString $os.OperatingSystemSKU) Architecture = (Get-SIString $os.OSArchitecture) InstallDate = (Normalize-DateTime $os.InstallDate) LastBootTime = (Normalize-DateTime $os.LastBootUpTime) RegisteredOrg = (Get-SIString $os.Organization) IsEndOfLifeOrNear = [bool]$eol.IsEol EndOfLifeKnown = [bool]$eol.Known EndOfLifeEvidence = (Get-SIString $eol.Evidence) }) } } catch { Write-Log -Level WARN -Message 'OperatingSystem collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- Hardware ------------------------------------------------------------- try { if ($cs -or $enclosure) { $raw.Hardware = [pscustomobject]([ordered]@{ Manufacturer = (Get-SIString $cs.Manufacturer) Model = (Get-SIString $cs.Model) SystemType = (Get-SIString $cs.SystemType) TotalPhysicalMemoryGB= (Convert-BytesToGB $cs.TotalPhysicalMemory) SerialNumber = (Get-SIString $enclosure.SerialNumber) AssetTag = (Get-SIString $enclosure.SMBIOSAssetTag) ChassisType = (Get-SIChassisType $enclosure.ChassisTypes) PhysicalProcessors = $cs.NumberOfProcessors LogicalProcessors = $cs.NumberOfLogicalProcessors }) } } catch { Write-Log -Level WARN -Message 'Hardware collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- BiosFirmware (BIOS, baseboard, Secure Boot, TPM) --------------------- try { $biosVersion = '' if ($bios -and $bios.BIOSVersion) { $biosVersion = (@($bios.BIOSVersion) -join '; ') } # Secure Boot (read-only). Confirm-SecureBootUEFI throws on legacy BIOS. $secureBoot = $null if (Get-CommandAvailable -Name 'Confirm-SecureBootUEFI') { try { $secureBoot = [bool](Confirm-SecureBootUEFI -ErrorAction Stop) } catch { $secureBoot = $null } } # TPM (read-only): prefer Get-Tpm, fall back to the microsofttpm WMI namespace. $tpmPresent = $false; $tpmEnabled = $null; $tpmSpec = '' if (Get-CommandAvailable -Name 'Get-Tpm') { try { $tpm = Get-Tpm -ErrorAction Stop if ($tpm) { if ($null -ne $tpm.TpmPresent) { $tpmPresent = [bool]$tpm.TpmPresent } elseif ($null -ne $tpm.TpmReady) { $tpmPresent = [bool]$tpm.TpmReady } if ($null -ne $tpm.TpmEnabled) { $tpmEnabled = [bool]$tpm.TpmEnabled } } } catch { } } if (-not $tpmPresent) { try { $wtpm = Get-SIFirst (Invoke-CimSafe -Namespace 'root/cimv2/security/microsofttpm' -ClassName 'Win32_Tpm') if ($wtpm) { $tpmPresent = $true if ($null -ne $wtpm.IsEnabled_InitialValue) { $tpmEnabled = [bool]$wtpm.IsEnabled_InitialValue } $tpmSpec = Get-SIString $wtpm.SpecVersion } } catch { } } $raw.BiosFirmware = [pscustomobject]([ordered]@{ Manufacturer = (Get-SIString $bios.Manufacturer) SMBIOSBIOSVersion = (Get-SIString $bios.SMBIOSBIOSVersion) BIOSVersion = (Get-SIString $biosVersion) ReleaseDate = (Normalize-DateTime $bios.ReleaseDate) BaseBoardManufacturer = (Get-SIString $baseboard.Manufacturer) BaseBoardProduct = (Get-SIString $baseboard.Product) BaseBoardVersion = (Get-SIString $baseboard.Version) SecureBootEnabled = $secureBoot TpmPresent = [bool]$tpmPresent TpmEnabled = $tpmEnabled TpmSpecVersion = (Get-SIString $tpmSpec) }) } catch { Write-Log -Level WARN -Message 'BiosFirmware collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- Processors ----------------------------------------------------------- try { $socketCount = @($procs).Count $procRows = [System.Collections.Generic.List[object]]::new() foreach ($p in @($procs)) { $procRows.Add([pscustomobject]([ordered]@{ Name = (Get-SIString $p.Name) Manufacturer = (Get-SIString $p.Manufacturer) SocketDesignation = (Get-SIString $p.SocketDesignation) NumberOfCores = $p.NumberOfCores NumberOfLogicalProcessors= $p.NumberOfLogicalProcessors MaxClockSpeed = $p.MaxClockSpeed SocketCount = $socketCount })) } $raw.Processors = @($procRows) } catch { Write-Log -Level WARN -Message 'Processors collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- Memory --------------------------------------------------------------- try { $memRows = [System.Collections.Generic.List[object]]::new() foreach ($m in @($mem)) { $memRows.Add([pscustomobject]([ordered]@{ DeviceLocator = (Get-SIString $m.DeviceLocator) CapacityGB = (Convert-BytesToGB $m.Capacity) Speed = $m.Speed Manufacturer = (Get-SIString $m.Manufacturer) PartNumber = (Get-SIString $m.PartNumber) FormFactor = $m.FormFactor })) } $raw.Memory = @($memRows) } catch { Write-Log -Level WARN -Message 'Memory collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- PageFile ------------------------------------------------------------- try { $autoManaged = $false if ($cs -and $null -ne $cs.AutomaticManagedPagefile) { $autoManaged = [bool]$cs.AutomaticManagedPagefile } $pfRows = [System.Collections.Generic.List[object]]::new() foreach ($pf in @($pagefiles)) { $pfRows.Add([pscustomobject]([ordered]@{ Name = (Get-SIString $pf.Name) AllocatedBaseSizeMB = $pf.AllocatedBaseSize CurrentUsageMB = $pf.CurrentUsage PeakUsageMB = $pf.PeakUsage AutomaticManaged = [bool]$autoManaged })) } if ($pfRows.Count -eq 0) { # No explicit page file usage entries; still record the managed state. $pfRows.Add([pscustomobject]([ordered]@{ Name = '' AllocatedBaseSizeMB = $null CurrentUsageMB = $null PeakUsageMB = $null AutomaticManaged = [bool]$autoManaged })) } $raw.PageFile = @($pfRows) } catch { Write-Log -Level WARN -Message 'PageFile collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- PendingReboot (registry only, read-only) ----------------------------- try { $cbs = Test-RegistryPathSafe -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Component Based Servicing\RebootPending' $wu = Test-RegistryPathSafe -Path 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired' $pfr = $false $pfrVal = Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager' -Name 'PendingFileRenameOperations' if ($null -ne $pfrVal -and @($pfrVal | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_) }).Count -gt 0) { $pfr = $true } $renamePending = $false try { $active = Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ActiveComputerName' -Name 'ComputerName' $pendName = Get-RegistryValueSafe -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\ComputerName\ComputerName' -Name 'ComputerName' if ($active -and $pendName -and ([string]$active -ne [string]$pendName)) { $renamePending = $true } } catch { } $indicators = [System.Collections.Generic.List[string]]::new() if ($cbs) { $indicators.Add('Component Based Servicing (RebootPending)') } if ($wu) { $indicators.Add('Windows Update (RebootRequired)') } if ($pfr) { $indicators.Add('PendingFileRenameOperations') } if ($renamePending) { $indicators.Add('Pending computer rename') } $rebootPending = ($cbs -or $wu -or $pfr -or $renamePending) $raw.PendingReboot = [pscustomobject]([ordered]@{ RebootPending = [bool]$rebootPending ComponentBasedServicing = [bool]$cbs WindowsUpdateRebootRequired = [bool]$wu PendingFileRenameOperations = [bool]$pfr PendingComputerRename = [bool]$renamePending Indicators = (@($indicators) -join '; ') }) } catch { Write-Log -Level WARN -Message 'PendingReboot collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- PhysicalVirtualDetection -------------------------------------------- try { $biosVerForDetect = '' if ($bios -and $bios.BIOSVersion) { $biosVerForDetect = (@($bios.BIOSVersion) -join ' ') } $det = Get-SIPlatformDetection -Manufacturer $cs.Manufacturer -Model $cs.Model -BiosManufacturer $bios.Manufacturer -BiosVersion ("{0} {1}" -f (Get-SIString $bios.SMBIOSBIOSVersion), $biosVerForDetect) -AssetTag $enclosure.SMBIOSAssetTag $raw.PhysicalVirtualDetection = [pscustomobject]([ordered]@{ Platform = (Get-SIString $det.Platform) IsVirtual = [bool]($det.Platform -and $det.Platform -notmatch '(?i)^Physical$') Evidence = (Get-SIString $det.Evidence) Confidence = (Get-SIString $det.Confidence) }) } catch { Write-Log -Level WARN -Message 'PhysicalVirtualDetection collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # --- HardwareVendor ------------------------------------------------------- try { $controllers = @(Invoke-CimSafe -ClassName 'Win32_SCSIController') $ctrlNames = @($controllers | ForEach-Object { Get-SIString $_.Name } | Where-Object { $_ } | Select-Object -Unique) $disks = @(Invoke-CimSafe -ClassName 'Win32_DiskDrive') $diskModels = @($disks | ForEach-Object { Get-SIString $_.Model } | Where-Object { $_ } | Select-Object -Unique) # Vendor management tool hints (service names). Read-only lookup only. $vendorHints = [System.Collections.Generic.List[string]]::new() try { $svcNames = @(Invoke-CimSafe -ClassName 'Win32_Service' -Property @('Name','DisplayName') | ForEach-Object { "{0} {1}" -f (Get-SIString $_.Name), (Get-SIString $_.DisplayName) }) $svcBlob = ($svcNames -join ' | ') if ($svcBlob -match '(?i)iDRAC|OpenManage|Dell') { $vendorHints.Add('Dell (iDRAC/OpenManage) service indicators present') } if ($svcBlob -match '(?i)iLO|Insight|HPE?|ProLiant') { $vendorHints.Add('HPE (iLO/Insight/ProLiant) service indicators present') } if ($svcBlob -match '(?i)XClarity|IMM|ThinkSystem|Lenovo') { $vendorHints.Add('Lenovo (XClarity/XCC/ThinkSystem) service indicators present') } } catch { } $raw.HardwareVendor = [pscustomobject]([ordered]@{ Manufacturer = (Get-SIString $cs.Manufacturer) Model = (Get-SIString $cs.Model) SerialNumber = (Get-SIString $enclosure.SerialNumber) StorageControllers = (@($ctrlNames) -join '; ') DiskDriveModels = (@($diskModels) -join '; ') VendorManagementHints = (@($vendorHints) -join '; ') WarrantyStatus = 'Unknown (requires vendor lookup; not performed - read-only, no internet access)' }) } catch { Write-Log -Level WARN -Message 'HardwareVendor collection failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } return $raw } #endregion #region Contract: dataset conversion ------------------------------------------ function ConvertTo-DiscoveryDatasets { param([object]$Context, $RawData) # Always emit ExecutionContext (RULE-CORE-NOTADMIN depends on it), even if # collection produced nothing - reconstruct from the context as a fallback. try { $ecRow = $null if ($RawData -and $RawData.ExecutionContext) { $ecRow = $RawData.ExecutionContext } if (-not $ecRow) { $cu = '' try { $cu = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name } catch { $cu = $env:USERNAME } $ecRow = [pscustomobject]([ordered]@{ IsAdmin = (ConvertTo-SIBool $Context.IsAdmin) IsSystem = (ConvertTo-SIBool $Context.IsSystem) PowerShellVersion = (Get-SIString $Context.PowerShellVersion) CurrentUser = $cu CurrentDirectory = '' Culture = '' TimeZone = '' RunId = (Get-SIString $Context.RunId) Mode = (Get-SIString $Context.Mode) ProjectType = (Get-SIString $Context.ProjectType) ComputerName = (Get-SIString $Context.ComputerName) }) } Add-DataSet -Context $Context -Name 'ExecutionContext' -Description 'Runtime execution context of this discovery run (privilege level, user, mode).' -Rows @($ecRow) -Visibility 'Internal' -SourceModule 'SystemInventory' | Out-Null } catch { Write-Log -Level WARN -Message 'ExecutionContext dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } if ($null -eq $RawData) { Add-Limitation -Context $Context -Module 'SystemInventory' -Message 'No raw system inventory data was returned; only a fallback ExecutionContext row was emitted.' -Impact 'Reduced system detail' | Out-Null return } # OperatingSystem try { if ($RawData.OperatingSystem) { Add-DataSet -Context $Context -Name 'OperatingSystem' -Description 'Operating system identity, build, install/boot dates, and end-of-life assessment.' -Rows @($RawData.OperatingSystem) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null if (-not $RawData.OperatingSystem.EndOfLifeKnown) { Add-Unknown -Context $Context -Unknown ("Support/end-of-life status of OS '{0}' could not be determined automatically." -f (Get-SIString $RawData.OperatingSystem.Caption)) -WhyItMatters 'An unsupported OS materially affects migration urgency, security posture, and scoping.' -Evidence (Get-SIString $RawData.OperatingSystem.EndOfLifeEvidence) -RecommendedValidationQuestion 'What is the support/lifecycle status of this operating system, and is a migration already planned?' -Module 'SystemInventory' | Out-Null } } } catch { Write-Log -Level WARN -Message 'OperatingSystem dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # Hardware try { if ($RawData.Hardware) { Add-DataSet -Context $Context -Name 'Hardware' -Description 'Chassis, manufacturer/model, serial/asset tag, and total physical memory.' -Rows @($RawData.Hardware) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } } catch { Write-Log -Level WARN -Message 'Hardware dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # BiosFirmware try { if ($RawData.BiosFirmware) { Add-DataSet -Context $Context -Name 'BiosFirmware' -Description 'BIOS/UEFI, baseboard, Secure Boot, and TPM firmware details.' -Rows @($RawData.BiosFirmware) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } } catch { Write-Log -Level WARN -Message 'BiosFirmware dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # Processors try { Add-DataSet -Context $Context -Name 'Processors' -Description 'Physical processor sockets with core/logical/clock details.' -Rows @($RawData.Processors) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } catch { Write-Log -Level WARN -Message 'Processors dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # Memory try { Add-DataSet -Context $Context -Name 'Memory' -Description 'Installed physical memory modules (per DIMM slot).' -Rows @($RawData.Memory) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } catch { Write-Log -Level WARN -Message 'Memory dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # PageFile try { Add-DataSet -Context $Context -Name 'PageFile' -Description 'Page file configuration and usage.' -Rows @($RawData.PageFile) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } catch { Write-Log -Level WARN -Message 'PageFile dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # PendingReboot try { if ($RawData.PendingReboot) { Add-DataSet -Context $Context -Name 'PendingReboot' -Description 'Pending-reboot indicators from Component Based Servicing, Windows Update, and file-rename operations.' -Rows @($RawData.PendingReboot) -Visibility 'Internal' -SourceModule 'SystemInventory' | Out-Null } } catch { Write-Log -Level WARN -Message 'PendingReboot dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # PhysicalVirtualDetection try { if ($RawData.PhysicalVirtualDetection) { Add-DataSet -Context $Context -Name 'PhysicalVirtualDetection' -Description 'Inferred physical vs. virtual/cloud platform for this server.' -Rows @($RawData.PhysicalVirtualDetection) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } } catch { Write-Log -Level WARN -Message 'PhysicalVirtualDetection dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } # HardwareVendor try { if ($RawData.HardwareVendor) { Add-DataSet -Context $Context -Name 'HardwareVendor' -Description 'Hardware vendor, serial, storage controllers, and management-tool hints (no warranty lookup).' -Rows @($RawData.HardwareVendor) -Visibility 'Both' -SourceModule 'SystemInventory' | Out-Null } } catch { Write-Log -Level WARN -Message 'HardwareVendor dataset build failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } } #endregion #region Contract: risk analysis (light) --------------------------------------- function Invoke-DiscoveryRiskAnalysis { param([object]$Context) # EOL OS is handled by RiskEngine rule RULE-OS-EOL (keyed off # OperatingSystem.IsEndOfLifeOrNear), so this stays light and only records # dependency/unknown context the rule engine does not derive. try { $pv = Get-SIDatasetRow -Context $Context -Name 'PhysicalVirtualDetection' if ($pv -and (ConvertTo-SIBool $pv.IsVirtual)) { Add-DependencyEdge -Context $Context -SourceType 'Server' -SourceName $Context.ComputerName -DependencyType 'HostedOn' -Target (Get-SIString $pv.Platform) -Evidence (Get-SIString $pv.Evidence) -Confidence (Get-SIString $pv.Confidence) -SourceDataset 'PhysicalVirtualDetection' -ProjectImpact 'Architecture Decision' -ValidationQuestion 'Which host/cluster or cloud subscription runs this VM, and is that platform in scope for the project?' | Out-Null } } catch { Write-Log -Level WARN -Message 'Virtualization dependency edge failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } try { $hv = Get-SIDatasetRow -Context $Context -Name 'HardwareVendor' $pv = Get-SIDatasetRow -Context $Context -Name 'PhysicalVirtualDetection' $isPhysical = (-not $pv) -or (-not (ConvertTo-SIBool $pv.IsVirtual)) if ($hv -and $isPhysical) { Add-Unknown -Context $Context -Unknown 'Hardware warranty / vendor support status cannot be determined from the server itself.' -WhyItMatters 'Out-of-warranty hardware affects migration urgency, risk tolerance, and whether hardware replacement belongs in scope.' -Evidence ("Manufacturer='{0}'; Model='{1}'; Serial='{2}'." -f (Get-SIString $hv.Manufacturer), (Get-SIString $hv.Model), (Get-SIString $hv.SerialNumber)) -RecommendedValidationQuestion 'What is the current warranty/support status for this hardware (by make/model/serial)?' -Module 'SystemInventory' | Out-Null } } catch { Write-Log -Level WARN -Message 'Warranty unknown record failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } } #endregion #region Contract: follow-up questions ----------------------------------------- function Get-DiscoveryFollowUpQuestions { param([object]$Context) try { $pv = Get-SIDatasetRow -Context $Context -Name 'PhysicalVirtualDetection' if ($pv -and (ConvertTo-SIBool $pv.IsVirtual)) { Add-FollowUpQuestion -Context $Context -Question ("This server appears to run on '{0}'. Which host/cluster or cloud platform is it on, and is that platform in scope?" -f (Get-SIString $pv.Platform)) -Category 'Infrastructure' -Module 'SystemInventory' -Audience 'Both' | Out-Null } elseif ($pv) { Add-FollowUpQuestion -Context $Context -Question 'This server appears to be physical hardware. What is its warranty/support status and expected refresh timeline?' -Category 'Infrastructure' -Module 'SystemInventory' -Audience 'Both' | Out-Null } } catch { Write-Log -Level WARN -Message 'Follow-up question generation failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } try { $os = Get-SIDatasetRow -Context $Context -Name 'OperatingSystem' if ($os -and (ConvertTo-SIBool $os.IsEndOfLifeOrNear)) { Add-FollowUpQuestion -Context $Context -Question ("The operating system '{0}' is at or near end of support. Is a migration or upgrade already planned, and by when?" -f (Get-SIString $os.Caption)) -Category 'Operating System' -Module 'SystemInventory' -Audience 'Both' | Out-Null } } catch { Write-Log -Level WARN -Message 'OS follow-up question generation failed.' -Module 'SystemInventory' -Exception $_ -Context $Context } } #endregion Export-ModuleMember -Function ` 'Get-DiscoveryModuleMetadata','Test-DiscoveryPrerequisites','Invoke-DiscoveryCollection', ` 'ConvertTo-DiscoveryDatasets','Invoke-DiscoveryRiskAnalysis','Get-DiscoveryFollowUpQuestions' |