Modules/Collectors/20-HardwareCollector.ps1

function Invoke-RangerHardwareCollector {
    param(
        [Parameter(Mandatory = $true)]
        [System.Collections.IDictionary]$Config,

        [Parameter(Mandatory = $true)]
        $CredentialMap,

        [Parameter(Mandatory = $true)]
        [object]$Definition,

        [Parameter(Mandatory = $true)]
        [string]$PackageRoot
    )

    $fixture = Get-RangerCollectorFixtureData -Config $Config -CollectorId $Definition.Id
    if ($fixture) {
        return ConvertTo-RangerHashtable -InputObject $fixture
    }

    if (-not $CredentialMap.bmc) {
        throw 'The hardware collector requires a BMC credential.'
    }

    $nodes = New-Object System.Collections.ArrayList
    $managementPosture = New-Object System.Collections.ArrayList
    $relationships = New-Object System.Collections.ArrayList
    $findings = New-Object System.Collections.ArrayList
    $rawEvidence = New-Object System.Collections.ArrayList

    $usableEndpoints = @(
        foreach ($endpoint in @($Config.targets.bmc.endpoints)) {
            if ($null -eq $endpoint) {
                continue
            }

            $host = if ($endpoint -is [System.Collections.IDictionary]) { $endpoint['host'] } else { $endpoint.host }
            if ([string]::IsNullOrWhiteSpace([string]$host)) {
                continue
            }

            [ordered]@{
                host = [string]$host
                node = if ($endpoint -is [System.Collections.IDictionary]) { $endpoint['node'] } else { $endpoint.node }
            }
        }
    )

    if ($usableEndpoints.Count -eq 0) {
        throw 'No usable BMC endpoints with a host value are configured.'
    }

    foreach ($endpoint in $usableEndpoints) {
        $host = $endpoint.host
        $nodeName = if ($endpoint.node) { $endpoint.node } else { $host }
        try {
            $systemUri = "https://$host/redfish/v1/Systems/System.Embedded.1"
            $system = Invoke-RangerRedfishRequest -Uri $systemUri -Credential $CredentialMap.bmc
            $bios = Invoke-RangerSafeAction -Label "Redfish BIOS inventory for $host" -DefaultValue $null -ScriptBlock { Invoke-RangerRedfishRequest -Uri "$systemUri/Bios" -Credential $CredentialMap.bmc }
            $manager = Invoke-RangerSafeAction -Label "Redfish manager inventory for $host" -DefaultValue $null -ScriptBlock { Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Managers/iDRAC.Embedded.1" -Credential $CredentialMap.bmc }
            $processors = @(
                Invoke-RangerSafeAction -Label "Redfish processor inventory for $host" -DefaultValue @() -ScriptBlock {
                    Invoke-RangerRedfishCollection -CollectionUri "$systemUri/Processors" -Host $host -Credential $CredentialMap.bmc
                }
            )
            $memory = @(
                Invoke-RangerSafeAction -Label "Redfish memory inventory for $host" -DefaultValue @() -ScriptBlock {
                    Invoke-RangerRedfishCollection -CollectionUri "$systemUri/Memory" -Host $host -Credential $CredentialMap.bmc
                }
            )
            $ethernet = @(
                Invoke-RangerSafeAction -Label "Redfish Ethernet inventory for $host" -DefaultValue @() -ScriptBlock {
                    Invoke-RangerRedfishCollection -CollectionUri "$systemUri/EthernetInterfaces" -Host $host -Credential $CredentialMap.bmc
                }
            )
            $storageControllers = @(
                Invoke-RangerSafeAction -Label "Redfish storage inventory for $host" -DefaultValue @() -ScriptBlock {
                    Invoke-RangerRedfishCollection -CollectionUri "$systemUri/Storage" -Host $host -Credential $CredentialMap.bmc
                }
            )
            $firmwareInventory = @(
                Invoke-RangerSafeAction -Label "Redfish firmware inventory for $host" -DefaultValue @() -ScriptBlock {
                    Invoke-RangerRedfishCollection -CollectionUri "https://$host/redfish/v1/UpdateService/FirmwareInventory" -Host $host -Credential $CredentialMap.bmc
                }
            )
            $updateService = Invoke-RangerSafeAction -Label "Redfish update service for $host" -DefaultValue $null -ScriptBlock { Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/UpdateService" -Credential $CredentialMap.bmc }
            $dellLcService = Invoke-RangerSafeAction -Label "Dell lifecycle controller service for $host" -DefaultValue $null -ScriptBlock { Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Managers/iDRAC.Embedded.1/Oem/Dell/DellLCService" -Credential $CredentialMap.bmc }
            $dellAttributes = Invoke-RangerSafeAction -Label "Dell manager attributes for $host" -DefaultValue $null -ScriptBlock { Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Managers/iDRAC.Embedded.1/Attributes" -Credential $CredentialMap.bmc }

            # Issue #57: Power supplies and thermal/fans from Redfish
            $powerSupplies = @(
                Invoke-RangerSafeAction -Label "Power supply inventory for $host" -DefaultValue @() -ScriptBlock {
                    $powerPayload = Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Chassis/System.Embedded.1/Power" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                    if ($powerPayload -and $powerPayload.PowerSupplies) {
                        @($powerPayload.PowerSupplies | ForEach-Object {
                            [ordered]@{
                                name                 = $_.Name
                                manufacturer         = $_.Manufacturer
                                model                = $_.Model
                                serialNumber         = $_.SerialNumber
                                partNumber           = $_.PartNumber
                                powerCapacityWatts   = $_.PowerCapacityWatts
                                lastPowerOutputWatts = $_.LastPowerOutputWatts
                                statusHealth         = if ($_.Status) { $_.Status.Health } else { $null }
                                statusState          = if ($_.Status) { $_.Status.State } else { $null }
                            }
                        })
                    } else { @() }
                }
            )
            $thermalPayload = Invoke-RangerSafeAction -Label "Thermal inventory for $host" -DefaultValue $null -ScriptBlock { Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Chassis/System.Embedded.1/Thermal" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue }
            $fans = if ($thermalPayload -and $thermalPayload.Fans) {
                @($thermalPayload.Fans | ForEach-Object {
                    [ordered]@{
                        name         = $_.Name
                        reading      = $_.Reading
                        readingUnits = $_.ReadingUnits
                        minReadingRange = $_.MinReadingRange
                        statusHealth = if ($_.Status) { $_.Status.Health } else { $null }
                        statusState  = if ($_.Status) { $_.Status.State } else { $null }
                    }
                })
            } else { @() }
            $temperatures = if ($thermalPayload -and $thermalPayload.Temperatures) {
                @($thermalPayload.Temperatures | Where-Object { -not [string]::IsNullOrWhiteSpace($_.Name) } | Select-Object -First 20 | ForEach-Object {
                    [ordered]@{
                        name                     = $_.Name
                        readingCelsius           = $_.ReadingCelsius
                        upperThresholdCritical   = $_.UpperThresholdCritical
                        upperThresholdNonCritical = $_.UpperThresholdNonCritical
                        statusHealth             = if ($_.Status) { $_.Status.Health } else { $null }
                        statusState              = if ($_.Status) { $_.Status.State } else { $null }
                    }
                })
            } else { @() }

            # Issue #57: NIC detail from Redfish NetworkAdapters endpoint
            $networkAdaptersDetail = @(
                Invoke-RangerSafeAction -Label "Redfish NetworkAdapters for $host" -DefaultValue @() -ScriptBlock {
                    $naCollection = Invoke-RangerRedfishRequest -Uri "$systemUri/NetworkAdapters" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                    if ($naCollection -and $naCollection.Members) {
                        @($naCollection.Members | ForEach-Object {
                            $naUri = $_.'@odata.id'
                            $na = Invoke-RangerSafeAction -Label "NetworkAdapter $naUri" -DefaultValue $null -ScriptBlock {
                                Invoke-RangerRedfishRequest -Uri "https://$host$naUri" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                            }
                            if ($na) {
                                [ordered]@{
                                    id              = $na.Id
                                    name            = $na.Name
                                    manufacturer    = $na.Manufacturer
                                    model           = $na.Model
                                    partNumber      = $na.PartNumber
                                    serialNumber    = $na.SerialNumber
                                    driverVersion   = if ($na.Controllers) { @($na.Controllers)[0].FirmwarePackageVersion } else { $null }
                                    firmwareVersion = if ($na.Controllers) { @($na.Controllers)[0].ControllerCapabilities.DataCenterBridging } else { $null }
                                    networkPorts    = @(if ($na.NetworkPorts) {
                                        Invoke-RangerSafeAction -Label "NetworkPorts for $naUri" -DefaultValue @() -ScriptBlock {
                                            $portsUri = "https://$host$naUri/NetworkPorts"
                                            $ports = Invoke-RangerRedfishRequest -Uri $portsUri -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                                            if ($ports -and $ports.Members) {
                                                @($ports.Members | ForEach-Object {
                                                    $p = Invoke-RangerSafeAction -Label "Port $_.'@odata.id'" -DefaultValue $null -ScriptBlock {
                                                        Invoke-RangerRedfishRequest -Uri "https://$host$($_.'@odata.id')" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                                                    }
                                                    if ($p) { [ordered]@{ id = $p.Id; linkStatus = $p.LinkStatus; currentLinkSpeedMbps = $p.CurrentLinkSpeedMbps; macAddress = $p.AssociatedNetworkAddresses; supportedLinkCapabilities = @($p.SupportedLinkCapabilities) } }
                                                } | Where-Object { $_ })
                                            } else { @() }
                                        }
                                    } else { @() })
                                    statusHealth = if ($na.Status) { $na.Status.Health } else { $null }
                                }
                            }
                        } | Where-Object { $_ })
                    } else { @() }
                }
            )

            # Per-DIMM granularity from Redfish memory collection
            $memoryDimms = @(
                Invoke-RangerSafeAction -Label "Redfish DIMM detail for $host" -DefaultValue @() -ScriptBlock {
                    @($memory | ForEach-Object {
                        $dimm = $_
                        [ordered]@{
                            id               = $dimm.'@odata.id'
                            manufacturer     = $dimm.Manufacturer
                            partNumber       = $dimm.PartNumber
                            serialNumber     = $dimm.SerialNumber
                            capacityMiB      = $dimm.CapacityMiB
                            capacityGiB      = if ($dimm.CapacityMiB) { [math]::Round($dimm.CapacityMiB / 1024.0, 2) } else { $null }
                            operatingSpeedMhz = $dimm.OperatingSpeedMhz
                            memoryType       = $dimm.MemoryType
                            memoryDeviceType = $dimm.MemoryDeviceType
                            bankLocator      = $dimm.BankLocator
                            slotLocator      = $dimm.DeviceLocator
                            configuredVoltageMv = $dimm.VoltageMV
                            status           = if ($dimm.Status) { [ordered]@{ state = $dimm.Status.State; health = $dimm.Status.Health } } else { $null }
                        }
                    })
                }
            )

            # GPU / Accelerators via Redfish PCIeDevices (and OS-side via WinRM if available)
            $gpuDevices = @(
                Invoke-RangerSafeAction -Label "GPU/accelerator inventory for $host" -DefaultValue @() -ScriptBlock {
                    $pcieCollection = Invoke-RangerSafeAction -Label "Redfish PCIe devices for $host" -DefaultValue $null -ScriptBlock {
                        Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Systems/System.Embedded.1/PCIeDevices" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                    }
                    if ($pcieCollection -and $pcieCollection.Members) {
                        @($pcieCollection.Members | ForEach-Object {
                            $pcieUri = $_.'@odata.id'
                            $pcieDev = Invoke-RangerSafeAction -Label "PCIe device detail $pcieUri" -DefaultValue $null -ScriptBlock {
                                Invoke-RangerRedfishRequest -Uri "https://$host$pcieUri" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                            }
                            if ($pcieDev -and $pcieDev.Name -match 'GPU|Accelerat|NVIDIA|AMD|Radeon') {
                                [ordered]@{
                                    name       = $pcieDev.Name
                                    model      = $pcieDev.Model
                                    deviceType = $pcieDev.DeviceType
                                    slotId     = if ($pcieDev.PCIeInterface) { $pcieDev.PCIeInterface.MaxLanes } else { $null }
                                    status     = if ($pcieDev.Status) { $pcieDev.Status.Health } else { $null }
                                }
                            }
                        } | Where-Object { $_ })
                    } else { @() }
                }
            )

            # BMC SSL certificate detail
            $bmcCert = Invoke-RangerSafeAction -Label "BMC SSL certificate for $host" -DefaultValue $null -ScriptBlock {
                $certCollection = Invoke-RangerRedfishRequest -Uri "https://$host/redfish/v1/Managers/iDRAC.Embedded.1/NetworkProtocol/HTTPS/Certificates" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                if ($certCollection -and $certCollection.Members -and @($certCollection.Members).Count -gt 0) {
                    $certUri = $certCollection.Members[0].'@odata.id'
                    $certDetail = Invoke-RangerRedfishRequest -Uri "https://$host$certUri" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                    if ($certDetail) {
                        $expiryDate = if ($certDetail.ValidNotAfter) { [datetime]::ParseExact($certDetail.ValidNotAfter, 'yyyy-MM-ddTHH:mm:ssZ', $null, [System.Globalization.DateTimeStyles]::AssumeUniversal) } else { $null }
                        [ordered]@{
                            subject        = $certDetail.Subject
                            issuer         = $certDetail.Issuer
                            validFrom      = $certDetail.ValidNotBefore
                            validUntil     = $certDetail.ValidNotAfter
                            daysUntilExpiry = if ($expiryDate) { [math]::Round(($expiryDate - (Get-Date)).TotalDays, 0) } else { $null }
                            thumbprint     = $certDetail.Fingerprint
                        }
                    }
                }
            }

            # Physical disk enumeration with slot/location depth
            $physicalDisksDetail = @(
                Invoke-RangerSafeAction -Label "Physical disk detail for $host" -DefaultValue @() -ScriptBlock {
                    @($storageControllers | ForEach-Object {
                        $ctrlUri = $_.'@odata.id'
                        if ($ctrlUri) {
                            $driveLinks = Invoke-RangerSafeAction -Label "Drive links for $ctrlUri" -DefaultValue $null -ScriptBlock {
                                Invoke-RangerRedfishRequest -Uri "https://$host$ctrlUri" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                            }
                            if ($driveLinks -and $driveLinks.Drives) {
                                @($driveLinks.Drives | ForEach-Object {
                                    $driveUri = $_.'@odata.id'
                                    $driveDetail = Invoke-RangerSafeAction -Label "Drive $driveUri" -DefaultValue $null -ScriptBlock {
                                        Invoke-RangerRedfishRequest -Uri "https://$host$driveUri" -Credential $CredentialMap.bmc -ErrorAction SilentlyContinue
                                    }
                                    if ($driveDetail) {
                                        [ordered]@{
                                            name            = $driveDetail.Name
                                            model           = $driveDetail.Model
                                            manufacturer    = $driveDetail.Manufacturer
                                            mediaType       = $driveDetail.MediaType
                                            protocol        = $driveDetail.Protocol
                                            capacityBytes   = $driveDetail.CapacityBytes
                                            capacityGiB     = if ($driveDetail.CapacityBytes) { [math]::Round($driveDetail.CapacityBytes / 1GB, 2) } else { $null }
                                            revision        = $driveDetail.Revision
                                            serialNumber    = $driveDetail.SerialNumber
                                            partNumber      = $driveDetail.PartNumber
                                            slot            = if ($driveDetail.PhysicalLocation) { $driveDetail.PhysicalLocation.PartLocation } else { $driveDetail.Location }
                                            locationIndicatorActive = $driveDetail.LocationIndicatorActive
                                            powerState      = $driveDetail.PowerState
                                            statusHealth    = if ($driveDetail.Status) { $driveDetail.Status.Health } else { $null }
                                            statusState     = if ($driveDetail.Status) { $driveDetail.Status.State } else { $null }
                                            predictedLifeLeftPercent = $driveDetail.PredictedLifeLeftPercent
                                            hotspareType    = $driveDetail.HotspareType
                                        }
                                    }
                                } | Where-Object { $_ })
                            } else { @() }
                        } else { @() }
                    })
                }
            )

            # VBS sub-components (OS-level — requires WinRM on the node)
            $vbsPosture = Invoke-RangerSafeAction -Label "VBS sub-component posture for $nodeName" -DefaultValue $null -ScriptBlock {
                Invoke-RangerClusterCommand -Config $Config -Credential $CredentialMap.cluster -NodeName $nodeName -ScriptBlock {
                    $dg = Get-CimInstance -Namespace 'root\Microsoft\Windows\DeviceGuard' -ClassName Win32_DeviceGuard -ErrorAction SilentlyContinue
                    # Issue #57: DDA and GPU-P capability
                    $partitionableGpu = @(try { Get-VMHostPartitionableGpu -ErrorAction Stop } catch { try { Get-VMPartitionableGpu -ErrorAction Stop } catch { @() } })
                    # Issue #70: OpenManage Integration for Microsoft Azure Local
                    $omiService = @(Get-Service -Name 'DELL_*', 'OpenManage*', 'OMHCI*' -ErrorAction SilentlyContinue | Select-Object Name, DisplayName, Status, StartType)
                    $omiReg = try { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Dell\OpenManage' -ErrorAction Stop } catch {
                        try { Get-ItemProperty -Path 'HKLM:\SOFTWARE\Dell\SEA\OMHCI' -ErrorAction Stop } catch { $null }
                    }
                    if ($dg) {
                        [ordered]@{
                            virtualBasedSecurityStatus    = $dg.VirtualizationBasedSecurityStatus
                            securityServicesRunning       = @($dg.SecurityServicesRunning)
                            securityServicesConfigured    = @($dg.SecurityServicesConfigured)
                            codeIntegrityPolicyEnforcementStatus = $dg.CodeIntegrityPolicyEnforcementStatus
                            usermodeCodeIntegrityPolicyEnforcementStatus = $dg.UsermodeCodeIntegrityPolicyEnforcementStatus
                            # 4 = HVCI, 6 = Credential Guard, 128 = DRTM indicator
                            hvciEnabled           = (@($dg.SecurityServicesRunning) -contains 4) -or (@($dg.SecurityServicesRunning) -contains 2)
                            credentialGuardEnabled = (@($dg.SecurityServicesRunning) -contains 1)
                            securedCoreDrtm        = (@($dg.SecurityServicesConfigured) -contains 128)
                            gpuPCapable            = @($partitionableGpu).Count -gt 0
                            gpuPartitionableCount  = @($partitionableGpu).Count
                            openManageService      = @($omiService | ForEach-Object { [ordered]@{ name = $_.Name; status = [string]$_.Status } })
                            openManageInstalled    = @($omiService).Count -gt 0
                            openManageVersion      = if ($omiReg) { $omiReg.Version } else { $null }
                        }
                    }
                }
            }

            # Issue #57: TPM detail extraction from system.TrustedModules
            $tpmDetail = if (@($system.TrustedModules).Count -gt 0) {
                $tpm = @($system.TrustedModules)[0]
                [ordered]@{
                    present         = $true
                    firmwareVersion = $tpm.FirmwareVersion
                    interfaceType   = $tpm.InterfaceType
                    statusHealth    = if ($tpm.Status) { $tpm.Status.Health } else { $null }
                    statusState     = if ($tpm.Status) { $tpm.Status.State } else { $null }
                }
            } else { [ordered]@{ present = $false } }

            # Issue #57: BIOS depth extraction
            $biosDetail = [ordered]@{
                version     = if ($bios.Attributes.SystemBiosVersion) { $bios.Attributes.SystemBiosVersion } else { $system.BiosVersion }
                vendor      = $bios.Attributes.SystemBiosVendor
                releaseDate = $bios.Attributes.SystemBiosReleaseDate
                bootMode    = if ($bios.Attributes.BootMode) { $bios.Attributes.BootMode } else { if ($system.Boot.BootSourceOverrideTarget) { 'UEFI' } else { $null } }
                secureBoot  = $bios.Attributes.SecureBoot
                uefiEnabled = $bios.Attributes.UefiBootSettings -eq 'Enabled' -or $bios.Attributes.BootMode -eq 'Uefi'
            }

            # Issue #70: Structured Dell OEM data
            $idracLicenseLevel = Invoke-RangerSafeAction -Label "iDRAC license level for $host" -DefaultValue $null -ScriptBlock {
                if ($dellAttributes -and $dellAttributes.Attributes) {
                    $attrs = $dellAttributes.Attributes
                    # Try common Dell attribute paths for license level
                    $lic = $attrs.'iDRAC.Embedded.1.LicensingSummary'
                    if (-not $lic) { $lic = $attrs.'LicenseSKU' }
                    if (-not $lic) { $lic = $attrs.'Info.1.License' }
                    $lic
                }
            }
            $lcVersion = Invoke-RangerSafeAction -Label "Lifecycle Controller version for $host" -DefaultValue $null -ScriptBlock {
                if ($dellLcService) {
                    $v = $dellLcService.LCVersion
                    if (-not $v) { $v = $dellLcService.Version }
                    if (-not $v -and $manager) { $manager.FirmwareVersion }
                    else { $v }
                }
            }
            $supportAssistDetail = Invoke-RangerSafeAction -Label "SupportAssist data for $host" -DefaultValue $null -ScriptBlock {
                if ($dellAttributes -and $dellAttributes.Attributes) {
                    $attrs = $dellAttributes.Attributes
                    [ordered]@{
                        enabled                 = [string]$attrs.'SupportAssist.1.Enable' -eq 'Enabled'
                        proSupportEntitlementDate = $attrs.'SupportAssist.1.ProPackageRenewalDate'
                        lastCollectionTime      = $attrs.'SupportAssist.1.CollectionTimeStamp'
                        contactEmail            = $attrs.'SupportAssist.1.ContactEmail'
                    }
                }
            }
            $firmwareComplianceDetail = @($firmwareInventory | ForEach-Object {
                $fw = $_
                [ordered]@{
                    name          = $fw.Name
                    id            = $fw.'@odata.id'
                    version       = $fw.Version
                    updateable    = $fw.Updateable
                    softwareType  = $fw.SoftwareType
                    releaseDate   = $fw.ReleaseDate
                    statusHealth  = if ($fw.Status) { $fw.Status.Health } else { $null }
                }
            })

            $trustedModules = @($system.TrustedModules)
            $processorModels = @(Get-RangerGroupedCount -Items $processors -PropertyName 'Model')
            $processorTotalCores = try { ($processors | Where-Object { $_.TotalCores } | Measure-Object -Property TotalCores -Sum).Sum } catch { $null }
            $processorLogicalThreads = try { ($processors | Where-Object { $_.TotalThreads } | Measure-Object -Property TotalThreads -Sum).Sum } catch { $null }
            $memoryCapacityGiB = [math]::Round((@($memory | Where-Object { $_.CapacityMiB } | ForEach-Object { [double]$_.CapacityMiB / 1024 } | Measure-Object -Sum).Sum), 2)
            $firmwareVersions = @(Get-RangerGroupedCount -Items $firmwareInventory -PropertyName 'Version')
            $storageControllerModels = @(Get-RangerGroupedCount -Items $storageControllers -PropertyName 'Name')

            [void]$nodes.Add([ordered]@{
                node             = $nodeName
                endpoint         = $host
                manufacturer     = $system.Manufacturer
                model            = $system.Model
                serviceTag       = $system.SKU
                serialNumber     = $system.SerialNumber
                powerState       = $system.PowerState
                biosVersion      = if ($bios.Attributes.SystemBiosVersion) { $bios.Attributes.SystemBiosVersion } else { $system.BiosVersion }
                biosDetail       = $biosDetail
                bmcFirmware      = $manager.FirmwareVersion
                cpuCount         = @($processors).Count
                memoryDeviceCount = @($memory).Count
                memoryGiB        = if ($system.MemorySummary.TotalSystemMemoryGiB) { $system.MemorySummary.TotalSystemMemoryGiB } else { $null }
                nicCount         = @($ethernet).Count
                storageControllerCount = @($storageControllers).Count
                firmwareCount    = @($firmwareInventory).Count
                processorSummary = [ordered]@{
                    sockets            = @($processors).Count
                    totalPhysicalCores = $processorTotalCores
                    logicalProcessors  = $processorLogicalThreads
                    models             = $processorModels
                }
                memorySummary    = [ordered]@{ dimmCount = @($memory).Count; totalCapacityGiB = $memoryCapacityGiB }
                memoryDimms      = @($memoryDimms)
                ethernetSummary  = [ordered]@{ portCount = @($ethernet).Count; byState = @(Get-RangerGroupedCount -Items $ethernet -PropertyName 'LinkStatus') }
                networkAdaptersDetail = @($networkAdaptersDetail)
                storageSummary   = [ordered]@{ controllerCount = @($storageControllers).Count; models = $storageControllerModels }
                physicalDisksDetail = @($physicalDisksDetail)
                firmwareSummary  = [ordered]@{ componentCount = @($firmwareInventory).Count; versions = $firmwareVersions }
                firmwareComplianceDetail = @($firmwareComplianceDetail)
                gpuDevices       = @($gpuDevices)
                gpuCount         = @($gpuDevices).Count
                powerSupplies    = @($powerSupplies)
                powerSupplyCount = @($powerSupplies).Count
                fans             = @($fans)
                fanCount         = @($fans).Count
                temperatures     = @($temperatures)
                tpmDetail        = $tpmDetail
                vbsPosture       = $vbsPosture
                bmcCert          = $bmcCert
                idracLicenseLevel = $idracLicenseLevel
                lcVersion        = $lcVersion
                supportAssist    = $supportAssistDetail
                securityPosture  = [ordered]@{
                    trustedModuleCount    = @($trustedModules).Count
                    secureBoot            = $bios.Attributes.SecureBoot
                    hvciEnabled           = if ($vbsPosture) { $vbsPosture.hvciEnabled } else { $null }
                    credentialGuardEnabled = if ($vbsPosture) { $vbsPosture.credentialGuardEnabled } else { $null }
                    bmcCertExpiryDays     = if ($bmcCert) { $bmcCert.daysUntilExpiry } else { $null }
                    tpmPresent            = $tpmDetail.present
                    tpmVersion            = $tpmDetail.interfaceType
                    gpuPCapable           = if ($vbsPosture) { $vbsPosture.gpuPCapable } else { $null }
                    openManageInstalled   = if ($vbsPosture) { $vbsPosture.openManageInstalled } else { $null }
                }
                trustedModules   = ConvertTo-RangerHashtable -InputObject $system.TrustedModules
                boot             = ConvertTo-RangerHashtable -InputObject $system.Boot
            })

            [void]$managementPosture.Add([ordered]@{
                node                    = $nodeName
                endpoint                = $host
                managerModel            = $manager.Model
                managerFirmwareVersion  = $manager.FirmwareVersion
                lifecycleController     = if ($manager.Name) { $manager.Name } else { 'iDRAC' }
                lifecycleControllerVersion = $lcVersion
                idracLicenseLevel       = $idracLicenseLevel
                openManageSignals       = ConvertTo-RangerHashtable -InputObject $manager.Oem
                openManageInstalled     = if ($vbsPosture) { $vbsPosture.openManageInstalled } else { $null }
                openManageVersion       = if ($vbsPosture) { $vbsPosture.openManageVersion } else { $null }
                firmwareInventoryCount  = @($firmwareInventory).Count
                firmwareComplianceDetail = @($firmwareComplianceDetail)
                lastResetTime           = $manager.DateTime
                updateService           = [ordered]@{ serviceEnabled = $updateService.ServiceEnabled; pushUri = $updateService.HttpPushUri; multipartPushUri = $updateService.MultipartHttpPushUri }
                lifecycleControllerState = ConvertTo-RangerHashtable -InputObject $dellLcService
                supportAssist           = $supportAssistDetail
                bmcCert                 = $bmcCert
            })

            [void]$relationships.Add((New-RangerRelationship -SourceType 'bmc-endpoint' -SourceId $host -TargetType 'cluster-node' -TargetId $nodeName -RelationshipType 'manages' -Properties ([ordered]@{ manufacturer = $system.Manufacturer; model = $system.Model })))
            [void]$rawEvidence.Add([ordered]@{
                node              = $nodeName
                system            = ConvertTo-RangerHashtable -InputObject $system
                bios              = ConvertTo-RangerHashtable -InputObject $bios
                manager           = ConvertTo-RangerHashtable -InputObject $manager
                updateService     = ConvertTo-RangerHashtable -InputObject $updateService
                lifecycleController = ConvertTo-RangerHashtable -InputObject $dellLcService
                managerAttributes = ConvertTo-RangerHashtable -InputObject $dellAttributes
                processors        = ConvertTo-RangerHashtable -InputObject $processors
                memory            = ConvertTo-RangerHashtable -InputObject $memory
                memoryDimms       = ConvertTo-RangerHashtable -InputObject $memoryDimms
                ethernet          = ConvertTo-RangerHashtable -InputObject $ethernet
                storageControllers = ConvertTo-RangerHashtable -InputObject $storageControllers
                physicalDisksDetail = ConvertTo-RangerHashtable -InputObject $physicalDisksDetail
                firmwareInventory = ConvertTo-RangerHashtable -InputObject $firmwareInventory
                gpuDevices        = ConvertTo-RangerHashtable -InputObject $gpuDevices
                vbsPosture        = ConvertTo-RangerHashtable -InputObject $vbsPosture
            })
        }
        catch {
            [void]$findings.Add((New-RangerFinding -Severity warning -Title "BMC endpoint unavailable for $nodeName" -Description $_.Exception.Message -AffectedComponents @($nodeName, $host) -CurrentState 'hardware collector partial' -Recommendation 'Confirm BMC reachability, Redfish availability, and credential validity.'))
        }
    }

    if ($nodes.Count -eq 0) {
        throw 'No hardware inventory could be gathered from the configured BMC endpoints.'
    }

    $nodesArray = @($nodes)
    $managementArray = @($managementPosture)
    $securityNodesWithoutTrustedModule = @($nodesArray | Where-Object { $_.securityPosture.trustedModuleCount -eq 0 })
    if ($securityNodesWithoutTrustedModule.Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity warning -Title 'One or more nodes do not report a trusted module through Redfish' -Description 'The hardware collector found nodes without a visible TPM or other trusted module signal in the Redfish system payload.' -AffectedComponents (@($securityNodesWithoutTrustedModule | ForEach-Object { $_.node })) -CurrentState 'trusted module metadata absent' -Recommendation 'Validate TPM posture and Redfish visibility for each affected node before relying on the hardware security inventory.'))
    }

    if (@($managementArray | Where-Object { -not $_.updateService.serviceEnabled }).Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity informational -Title 'One or more Dell management endpoints did not advertise update-service enablement' -Description 'The OEM posture snapshot could not confirm a healthy Redfish update-service path for every endpoint.' -AffectedComponents (@($managementArray | Where-Object { -not $_.updateService.serviceEnabled } | ForEach-Object { $_.node })) -CurrentState 'firmware-service posture mixed' -Recommendation 'Review iDRAC update-service state, firmware catalog access, and lifecycle-controller posture before relying on automated firmware evidence.'))
    }

    $nodesWithExpiringBmcCert = @($nodesArray | Where-Object { $null -ne $_.bmcCert.daysUntilExpiry -and $_.bmcCert.daysUntilExpiry -lt 90 })
    if ($nodesWithExpiringBmcCert.Count -gt 0) {
        [void]$findings.Add((New-RangerFinding -Severity warning -Title 'BMC SSL certificate expiring within 90 days on one or more nodes' -Description 'The iDRAC certificate inventory found certificates approaching expiry.' -AffectedComponents (@($nodesWithExpiringBmcCert | ForEach-Object { $_.node })) -CurrentState "$($nodesWithExpiringBmcCert.Count) nodes with expiring BMC cert" -Recommendation 'Renew the iDRAC HTTPS certificate before it expires to prevent management plane disruption.'))
    }

    return @{
        Status        = if ($findings.Count -gt 0) { 'partial' } else { 'success' }
        Domains       = @{
            hardware = [ordered]@{
                nodes   = $nodesArray
                summary = [ordered]@{
                    nodeCount     = $nodes.Count
                    manufacturers = @(Get-RangerGroupedCount -Items $nodesArray -PropertyName 'manufacturer')
                    models        = @(Get-RangerGroupedCount -Items $nodesArray -PropertyName 'model')
                    firmwareNodes = @($managementArray | Where-Object { $_.firmwareInventoryCount -gt 0 }).Count
                    totalMemoryGiB = [math]::Round((@($nodesArray | Where-Object { $null -ne $_.memoryGiB } | Measure-Object -Property memoryGiB -Sum).Sum), 2)
                    totalProcessors = @($nodesArray | Measure-Object -Property cpuCount -Sum).Sum
                    gpuNodes      = @($nodesArray | Where-Object { $_.gpuCount -gt 0 }).Count
                    totalGpuCount = @($nodesArray | Measure-Object -Property gpuCount -Sum).Sum
                }
                firmware = [ordered]@{ managedNodes = @($managementArray | Where-Object { $_.managerFirmwareVersion }).Count; versions = @(Get-RangerGroupedCount -Items $managementArray -PropertyName 'managerFirmwareVersion') }
                security = [ordered]@{
                    trustedModuleNodes     = @($nodesArray | Where-Object { $_.securityPosture.trustedModuleCount -gt 0 }).Count
                    secureBootEnabledNodes = @($nodesArray | Where-Object { $_.securityPosture.secureBoot -in @('Enabled', 'On', $true) }).Count
                    hvciEnabledNodes       = @($nodesArray | Where-Object { $_.securityPosture.hvciEnabled -eq $true }).Count
                    credGuardEnabledNodes  = @($nodesArray | Where-Object { $_.securityPosture.credentialGuardEnabled -eq $true }).Count
                    bmcCertExpiringNodes   = @($nodesArray | Where-Object { $null -ne $_.securityPosture.bmcCertExpiryDays -and $_.securityPosture.bmcCertExpiryDays -lt 90 }).Count
                }
            }
            oemIntegration = [ordered]@{
                endpoints         = @($Config.targets.bmc.endpoints)
                managementPosture = $managementArray
            }
        }
        Findings      = @($findings)
        Relationships = @($relationships)
        RawEvidence   = @($rawEvidence)
    }
}