Загрузка данных


#requires -Version 5.1

Import-Module ActiveDirectory

# =========================
# НАСТРОЙКИ
# =========================

$OutputDir = "C:\Temp\CryptoInventory"

# Если нужно ограничить OU, укажи SearchBase, например:
# $SearchBase = "OU=Workstations,DC=domain,DC=local"
$SearchBase = ""

# Включать серверы в инвентаризацию?
$IncludeServers = $false

# Выгружать полный серийный номер в CSV?
# Лучше оставить $false. Для подсчёта достаточно LicenseHash.
$IncludeRawLicense = $false

# Если ICMP/ping запрещён, поставь $false
$UsePingPrecheck = $true

# =========================
# ПОДГОТОВКА
# =========================

if (-not (Test-Path $OutputDir)) {
    New-Item -Path $OutputDir -ItemType Directory -Force | Out-Null
}

$DetailCsv     = Join-Path $OutputDir "CryptoInventory_Detail.csv"
$SummaryCsv    = Join-Path $OutputDir "CryptoInventory_Summary.csv"
$DuplicatesCsv = Join-Path $OutputDir "CryptoInventory_Duplicates.csv"

$AdParams = @{
    Filter     = 'Enabled -eq $true'
    Properties = @('OperatingSystem', 'DNSHostName')
}

if (-not [string]::IsNullOrWhiteSpace($SearchBase)) {
    $AdParams.SearchBase = $SearchBase
}

$Computers = Get-ADComputer @AdParams |
    Where-Object {
        $_.OperatingSystem -match "Windows" -and
        ($IncludeServers -or $_.OperatingSystem -notmatch "Server")
    } |
    Sort-Object Name

Write-Host "Найдено компьютеров для проверки: $($Computers.Count)" -ForegroundColor Cyan

# =========================
# УДАЛЁННЫЙ БЛОК
# =========================

$InventoryBlock = {
    param(
        [bool]$IncludeRawLicense
    )

    $ErrorActionPreference = "SilentlyContinue"
    $script:Rows = @()

    function Normalize-License {
        param([string]$Value)

        if ([string]::IsNullOrWhiteSpace($Value)) {
            return ""
        }

        return (($Value -replace '[\s\-]', '').Trim()).ToUpperInvariant()
    }

    function Get-LicenseHash {
        param([string]$Value)

        $Normalized = Normalize-License $Value

        if ([string]::IsNullOrWhiteSpace($Normalized)) {
            return ""
        }

        try {
            $Sha = [System.Security.Cryptography.SHA256]::Create()
            $Bytes = [System.Text.Encoding]::UTF8.GetBytes($Normalized)
            return ([BitConverter]::ToString($Sha.ComputeHash($Bytes))).Replace("-", "")
        }
        catch {
            return ""
        }
        finally {
            if ($Sha) {
                $Sha.Dispose()
            }
        }
    }

    function Mask-License {
        param([string]$Value)

        $Normalized = Normalize-License $Value

        if ([string]::IsNullOrWhiteSpace($Normalized)) {
            return ""
        }

        if ($Normalized.Length -le 12) {
            return $Normalized
        }

        return ($Normalized.Substring(0, 5) + "..." + $Normalized.Substring($Normalized.Length - 5))
    }

    function Join-Values {
        param(
            [object[]]$Items,
            [string]$Property
        )

        return @(
            $Items |
                ForEach-Object { $_.$Property } |
                Where-Object { -not [string]::IsNullOrWhiteSpace($_) } |
                Sort-Object -Unique
        ) -join "; "
    }

    function Add-InventoryRow {
        param(
            [string]$Product,
            [string]$Component,
            [string]$InstalledNames,
            [string]$InstalledVersions,
            [string]$LicenseValue,
            [string]$Source,
            [string]$Status,
            [string]$Details
        )

        $script:Rows += [PSCustomObject]@{
            ComputerName      = $env:COMPUTERNAME
            Product           = $Product
            Component         = $Component
            InstalledNames    = $InstalledNames
            InstalledVersions = $InstalledVersions
            LicenseMasked     = Mask-License $LicenseValue
            LicenseHash       = Get-LicenseHash $LicenseValue
            LicenseRaw        = if ($IncludeRawLicense -and -not [string]::IsNullOrWhiteSpace($LicenseValue)) { $LicenseValue.Trim() } else { "" }
            Source            = $Source
            Status            = $Status
            Details           = $Details
        }
    }

    function Test-LicenseLikeValue {
        param([string]$Value)

        $Normalized = Normalize-License $Value

        if ([string]::IsNullOrWhiteSpace($Normalized)) {
            return $false
        }

        # Обычно серийники/ключи — алфавитно-цифровые строки.
        # Ограничиваем, чтобы не собрать случайные пути/описания.
        if ($Normalized.Length -lt 20) {
            return $false
        }

        if ($Normalized.Length -gt 120) {
            return $false
        }

        if ($Normalized -notmatch '^[A-Z0-9]+$') {
            return $false
        }

        return $true
    }

    function Infer-CryptoProComponent {
        param(
            [string]$Path,
            [string]$PropertyName,
            [string]$DisplayName
        )

        $Text = "$Path $PropertyName $DisplayName"

        if ($Text -match "OCSP") {
            return "OCSP Client"
        }

        if ($Text -match "TSP") {
            return "TSP Client"
        }

        return "CSP"
    }

    function Search-RegistryLicenseValues {
        param(
            [string[]]$Roots,
            [string]$DefaultProduct,
            [string]$DefaultComponent,
            [string]$InstalledNames,
            [string]$InstalledVersions
        )

        foreach ($Root in $Roots) {
            if (-not (Test-Path $Root)) {
                continue
            }

            Get-ChildItem -Path $Root -Recurse -ErrorAction SilentlyContinue | ForEach-Object {
                $Key = $_
                $Props = Get-ItemProperty -Path $Key.PSPath -ErrorAction SilentlyContinue

                if (-not $Props) {
                    return
                }

                foreach ($Prop in $Props.PSObject.Properties) {
                    if ($Prop.Name -match '^PS(Path|ParentPath|ChildName|Drive|Provider)$') {
                        continue
                    }

                    if ($null -eq $Prop.Value) {
                        continue
                    }

                    if ($Prop.Value -isnot [string]) {
                        continue
                    }

                    $Value = [string]$Prop.Value

                    $InterestingName = $Prop.Name -match '(ProductID|PIDKEY|License|Licence|Serial|TSP|OCSP)'
                    $InterestingPath = $Key.Name -match '(Crypto|Крипто|Trusted|Digt|License|TSP|OCSP|CSP)'

                    if (($InterestingName -or $InterestingPath) -and (Test-LicenseLikeValue $Value)) {
                        $Component = $DefaultComponent

                        if ($DefaultProduct -match "CryptoPro") {
                            $Component = Infer-CryptoProComponent -Path $Key.Name -PropertyName $Prop.Name -DisplayName ""
                        }

                        Add-InventoryRow `
                            -Product $DefaultProduct `
                            -Component $Component `
                            -InstalledNames $InstalledNames `
                            -InstalledVersions $InstalledVersions `
                            -LicenseValue $Value `
                            -Source "$($Key.Name)\$($Prop.Name)" `
                            -Status "License-like value found" `
                            -Details "Найдено в реестре"
                    }
                }
            }
        }
    }

    # =========================
    # УСТАНОВЛЕННЫЕ ПРОГРАММЫ
    # =========================

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

    $InstalledApps = Get-ItemProperty $UninstallPaths -ErrorAction SilentlyContinue |
        Where-Object {
            -not [string]::IsNullOrWhiteSpace($_.DisplayName)
        }

    $CryptoApps = $InstalledApps |
        Where-Object {
            $_.DisplayName -match "КриптоПро|CryptoPro|Crypto Pro|Crypto-Pro|КриптоАРМ|CryptoARM|Trusted Desktop|TSP Client|OCSP Client"
        }

    $CspApps = $CryptoApps |
        Where-Object {
            $_.DisplayName -match "КриптоПро CSP|CryptoPro CSP|Crypto Pro CSP|Crypto-Pro CSP"
        }

    $CryptoArmApps = $CryptoApps |
        Where-Object {
            $_.DisplayName -match "КриптоАРМ|CryptoARM|Trusted Desktop"
        }

    $TspApps = $CryptoApps |
        Where-Object {
            $_.DisplayName -match "TSP Client"
        }

    $OcspApps = $CryptoApps |
        Where-Object {
            $_.DisplayName -match "OCSP Client"
        }

    $CspInstalledNames      = Join-Values -Items $CspApps -Property "DisplayName"
    $CspInstalledVersions   = Join-Values -Items $CspApps -Property "DisplayVersion"
    $ArmInstalledNames      = Join-Values -Items $CryptoArmApps -Property "DisplayName"
    $ArmInstalledVersions   = Join-Values -Items $CryptoArmApps -Property "DisplayVersion"
    $TspInstalledNames      = Join-Values -Items $TspApps -Property "DisplayName"
    $TspInstalledVersions   = Join-Values -Items $TspApps -Property "DisplayVersion"
    $OcspInstalledNames     = Join-Values -Items $OcspApps -Property "DisplayName"
    $OcspInstalledVersions  = Join-Values -Items $OcspApps -Property "DisplayVersion"

    # =========================
    # КРИПТОПРО CSP / TSP / OCSP
    # =========================

    # Известные MSI InstallProperties для CSP 4.x и 5.x.
    # Даже если эти пути не сработают на конкретной сборке, ниже есть общий обход Installer\UserData.
    $KnownCryptoProProductPaths = @(
        [PSCustomObject]@{
            Product   = "CryptoPro CSP"
            Component = "CSP"
            Path      = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\7AB5E7046046FB044ACD63458B5F481C\InstallProperties"
            ValueName = "ProductID"
        },
        [PSCustomObject]@{
            Product   = "CryptoPro CSP"
            Component = "CSP"
            Path      = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\08F19F05793DC7340B8C2621D83E5BE5\InstallProperties"
            ValueName = "ProductID"
        }
    )

    foreach ($Item in $KnownCryptoProProductPaths) {
        $Reg = Get-ItemProperty -Path $Item.Path -ErrorAction SilentlyContinue

        if ($Reg -and $Reg.$($Item.ValueName)) {
            Add-InventoryRow `
                -Product $Item.Product `
                -Component $Item.Component `
                -InstalledNames $CspInstalledNames `
                -InstalledVersions $CspInstalledVersions `
                -LicenseValue $Reg.$($Item.ValueName) `
                -Source "$($Item.Path)\$($Item.ValueName)" `
                -Status "License found" `
                -Details "Known CryptoPro CSP InstallProperties path"
        }
    }

    # Общий поиск по Windows Installer UserData.
    $InstallerRoot = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products"

    if (Test-Path $InstallerRoot) {
        Get-ChildItem -Path $InstallerRoot -ErrorAction SilentlyContinue | ForEach-Object {
            $InstallPropertiesPath = Join-Path $_.PSPath "InstallProperties"
            $Props = Get-ItemProperty -Path $InstallPropertiesPath -ErrorAction SilentlyContinue

            if (-not $Props) {
                return
            }

            $DisplayName = [string]$Props.DisplayName
            $ProductID = [string]$Props.ProductID

            if ([string]::IsNullOrWhiteSpace($ProductID)) {
                return
            }

            if ($DisplayName -match "КриптоПро|CryptoPro|Crypto Pro|Crypto-Pro|TSP Client|OCSP Client") {
                $Component = Infer-CryptoProComponent -Path $InstallPropertiesPath -PropertyName "ProductID" -DisplayName $DisplayName

                $ProductName = switch ($Component) {
                    "TSP Client"  { "CryptoPro TSP Client" }
                    "OCSP Client" { "CryptoPro OCSP Client" }
                    default       { "CryptoPro CSP" }
                }

                $Names = switch ($Component) {
                    "TSP Client"  { $TspInstalledNames }
                    "OCSP Client" { $OcspInstalledNames }
                    default       { $CspInstalledNames }
                }

                $Versions = switch ($Component) {
                    "TSP Client"  { $TspInstalledVersions }
                    "OCSP Client" { $OcspInstalledVersions }
                    default       { $CspInstalledVersions }
                }

                Add-InventoryRow `
                    -Product $ProductName `
                    -Component $Component `
                    -InstalledNames $Names `
                    -InstalledVersions $Versions `
                    -LicenseValue $ProductID `
                    -Source "$InstallPropertiesPath\ProductID" `
                    -Status "License found" `
                    -Details "Windows Installer InstallProperties"
            }
        }
    }

    # Дополнительный поиск по веткам КриптоПро.
    # Это особенно полезно для TSP/OCSP, где лицензия может не выглядеть как отдельное приложение.
    $CryptoProRoots = @(
        "HKLM:\SOFTWARE\Crypto Pro",
        "HKLM:\SOFTWARE\WOW6432Node\Crypto Pro",
        "HKLM:\SOFTWARE\Crypto-Pro",
        "HKLM:\SOFTWARE\WOW6432Node\Crypto-Pro"
    )

    Search-RegistryLicenseValues `
        -Roots $CryptoProRoots `
        -DefaultProduct "CryptoPro CSP" `
        -DefaultComponent "CSP" `
        -InstalledNames $CspInstalledNames `
        -InstalledVersions $CspInstalledVersions

    # =========================
    # КРИПТОАРМ / КРИПТОАРМ ГОСТ
    # =========================

    # КриптоАРМ 5 / Trusted Desktop
    $CryptoArm5Paths = @(
        "HKLM:\SOFTWARE\WOW6432Node\Digt\Trusted Desktop\License",
        "HKLM:\SOFTWARE\Digt\Trusted Desktop\License"
    )

    foreach ($Path in $CryptoArm5Paths) {
        $Lic = Get-ItemProperty -Path $Path -ErrorAction SilentlyContinue

        if ($Lic.SerialNumber) {
            Add-InventoryRow `
                -Product "CryptoARM"
                -Component "CryptoARM 5 / Trusted Desktop" `
                -InstalledNames $ArmInstalledNames `
                -InstalledVersions $ArmInstalledVersions `
                -LicenseValue $Lic.SerialNumber `
                -Source "$Path\SerialNumber" `
                -Status "License found" `
                -Details "Trusted Desktop registry license"
        }
    }

    # КриптоАРМ ГОСТ 2.5 / 3 через HKLM
    $CryptoArmGostRegPaths = @(
        "HKLM:\SOFTWARE\Trusted\CryptoARM GOST",
        "HKLM:\SOFTWARE\Trusted\CryptoARM GOST 3",
        "HKLM:\SOFTWARE\WOW6432Node\Trusted\CryptoARM GOST",
        "HKLM:\SOFTWARE\WOW6432Node\Trusted\CryptoARM GOST 3"
    )

    foreach ($Path in $CryptoArmGostRegPaths) {
        $Lic = Get-ItemProperty -Path $Path -ErrorAction SilentlyContinue

        if ($Lic.license) {
            Add-InventoryRow `
                -Product "CryptoARM"
                -Component "CryptoARM GOST" `
                -InstalledNames $ArmInstalledNames `
                -InstalledVersions $ArmInstalledVersions `
                -LicenseValue $Lic.license `
                -Source "$Path\license" `
                -Status "License found" `
                -Details "CryptoARM GOST registry license"
        }
    }

    # КриптоАРМ ГОСТ через license.lic в профилях пользователей
    $UserProfiles = Get-ChildItem "C:\Users" -Directory -ErrorAction SilentlyContinue |
        Where-Object {
            $_.Name -notin @("Default", "Default User", "Public", "All Users")
        }

    foreach ($Profile in $UserProfiles) {
        $LicenseFiles = @(
            Join-Path $Profile.FullName "AppData\Local\Trusted\CryptoARM GOST\license.lic"
            Join-Path $Profile.FullName "AppData\Local\Trusted\CryptoARM GOST 3\license.lic"
            Join-Path $Profile.FullName "AppData\Local\Trusted\CryptoARM\license.lic"
            Join-Path $Profile.FullName "AppData\Local\Trusted\CryptoARM 3\license.lic"
        )

        foreach ($File in $LicenseFiles) {
            if (Test-Path $File) {
                $Content = Get-Content $File -Raw -ErrorAction SilentlyContinue

                if (-not [string]::IsNullOrWhiteSpace($Content)) {
                    Add-InventoryRow `
                        -Product "CryptoARM" `
                        -Component "CryptoARM GOST / file license" `
                        -InstalledNames $ArmInstalledNames `
                        -InstalledVersions $ArmInstalledVersions `
                        -LicenseValue $Content `
                        -Source $File `
                        -Status "License file found" `
                        -Details "User profile: $($Profile.Name)"
                }
            }
        }
    }

    # Дополнительный поиск по веткам Trusted / Digt.
    $CryptoArmRoots = @(
        "HKLM:\SOFTWARE\Trusted",
        "HKLM:\SOFTWARE\WOW6432Node\Trusted",
        "HKLM:\SOFTWARE\Digt",
        "HKLM:\SOFTWARE\WOW6432Node\Digt"
    )

    Search-RegistryLicenseValues `
        -Roots $CryptoArmRoots `
        -DefaultProduct "CryptoARM" `
        -DefaultComponent "CryptoARM / unknown component" `
        -InstalledNames $ArmInstalledNames `
        -InstalledVersions $ArmInstalledVersions

    # =========================
    # СТАТУСЫ, ЕСЛИ ПРОДУКТ ЕСТЬ, А ЛИЦЕНЗИЯ НЕ НАЙДЕНА
    # =========================

    $HasCspLicense = @($script:Rows | Where-Object {
        $_.Product -eq "CryptoPro CSP" -and $_.Component -eq "CSP" -and $_.LicenseHash
    }).Count -gt 0

    $HasTspLicense = @($script:Rows | Where-Object {
        $_.Component -eq "TSP Client" -and $_.LicenseHash
    }).Count -gt 0

    $HasOcspLicense = @($script:Rows | Where-Object {
        $_.Component -eq "OCSP Client" -and $_.LicenseHash
    }).Count -gt 0

    $HasArmLicense = @($script:Rows | Where-Object {
        $_.Product -eq "CryptoARM" -and $_.LicenseHash
    }).Count -gt 0

    if ($CspApps -and -not $HasCspLicense) {
        Add-InventoryRow `
            -Product "CryptoPro CSP" `
            -Component "CSP" `
            -InstalledNames $CspInstalledNames `
            -InstalledVersions $CspInstalledVersions `
            -LicenseValue "" `
            -Source "" `
            -Status "Installed, license not found" `
            -Details "КриптоПро CSP установлен, но лицензия не найдена в проверенных местах"
    }

    if (($CspApps -or $TspApps) -and -not $HasTspLicense) {
        Add-InventoryRow `
            -Product "CryptoPro TSP Client" `
            -Component "TSP Client" `
            -InstalledNames $TspInstalledNames `
            -InstalledVersions $TspInstalledVersions `
            -LicenseValue "" `
            -Source "" `
            -Status "License not found" `
            -Details "Не найден ключ TSP Client. Это не доказывает отсутствие лицензии, но требует ручной проверки"
    }

    if (($CspApps -or $OcspApps) -and -not $HasOcspLicense) {
        Add-InventoryRow `
            -Product "CryptoPro OCSP Client" `
            -Component "OCSP Client" `
            -InstalledNames $OcspInstalledNames `
            -InstalledVersions $OcspInstalledVersions `
            -LicenseValue "" `
            -Source "" `
            -Status "License not found" `
            -Details "Не найден ключ OCSP Client. Это не доказывает отсутствие лицензии, но требует ручной проверки"
    }

    if ($CryptoArmApps -and -not $HasArmLicense) {
        Add-InventoryRow `
            -Product "CryptoARM" `
            -Component "CryptoARM" `
            -InstalledNames $ArmInstalledNames `
            -InstalledVersions $ArmInstalledVersions `
            -LicenseValue "" `
            -Source "" `
            -Status "Installed, license not found" `
            -Details "КриптоАРМ установлен, но лицензия не найдена в реестре/профилях"
    }

    if ($CryptoApps.Count -eq 0 -and $script:Rows.Count -eq 0) {
        Add-InventoryRow `
            -Product "Inventory" `
            -Component "" `
            -InstalledNames "" `
            -InstalledVersions "" `
            -LicenseValue "" `
            -Source "" `
            -Status "No target crypto products found" `
            -Details "Не найдены КриптоПро/КриптоАРМ/TSP/OCSP"
    }

    # Убираем полные дубли строк
    $script:Rows |
        Group-Object ComputerName, Product, Component, LicenseHash, Source, Status |
        ForEach-Object { $_.Group[0] }
}

# =========================
# ЗАПУСК ПО ПК
# =========================

$AllResults = @()

foreach ($Computer in $Computers) {
    $Target = if (-not [string]::IsNullOrWhiteSpace($Computer.DNSHostName)) {
        $Computer.DNSHostName
    }
    else {
        $Computer.Name
    }

    Write-Host "Проверка: $($Computer.Name)" -ForegroundColor Cyan

    if ($UsePingPrecheck) {
        $PingOk = Test-Connection -ComputerName $Target -Count 1 -Quiet -ErrorAction SilentlyContinue

        if (-not $PingOk) {
            $AllResults += [PSCustomObject]@{
                ComputerName      = $Computer.Name
                Product           = "Inventory"
                Component         = ""
                InstalledNames    = ""
                InstalledVersions = ""
                LicenseMasked     = ""
                LicenseHash       = ""
                LicenseRaw        = ""
                Source            = ""
                Status            = "Offline or ICMP blocked"
                Details           = "ПК не ответил на ping. Если ICMP запрещён, поставь `$UsePingPrecheck = `$false"
            }

            continue
        }
    }

    try {
        $RemoteResult = Invoke-Command `
            -ComputerName $Target `
            -ScriptBlock $InventoryBlock `
            -ArgumentList $IncludeRawLicense `
            -ErrorAction Stop

        $AllResults += $RemoteResult
    }
    catch {
        $AllResults += [PSCustomObject]@{
            ComputerName      = $Computer.Name
            Product           = "Inventory"
            Component         = ""
            InstalledNames    = ""
            InstalledVersions = ""
            LicenseMasked     = ""
            LicenseHash       = ""
            LicenseRaw        = ""
            Source            = ""
            Status            = "Remote error"
            Details           = $_.Exception.Message
        }
    }
}

# =========================
# ЭКСПОРТ ДЕТАЛЕЙ
# =========================

$AllResults |
    Sort-Object ComputerName, Product, Component, Status |
    Export-Csv -Path $DetailCsv -NoTypeInformation -Encoding UTF8

# =========================
# СВОДКА ПО УНИКАЛЬНЫМ ЛИЦЕНЗИЯМ
# =========================

$LicenseSummary = $AllResults |
    Where-Object { -not [string]::IsNullOrWhiteSpace($_.LicenseHash) } |
    Group-Object Product, Component, LicenseHash |
    ForEach-Object {
        $Group = $_.Group
        $ComputersWithLicense = @(
            $Group.ComputerName |
                Sort-Object -Unique
        )

        [PSCustomObject]@{
            Product       = $Group[0].Product
            Component     = $Group[0].Component
            LicenseMasked = $Group[0].LicenseMasked
            LicenseHash   = $Group[0].LicenseHash
            ComputerCount = $ComputersWithLicense.Count
            Computers     = $ComputersWithLicense -join ", "
            Sources       = (@($Group.Source | Sort-Object -Unique) -join " | ")
        }
    } |
    Sort-Object Product, Component, ComputerCount -Descending

$LicenseSummary |
    Export-Csv -Path $SummaryCsv -NoTypeInformation -Encoding UTF8

# =========================
# ДУБЛИ ЛИЦЕНЗИЙ
# =========================

$LicenseSummary |
    Where-Object { $_.ComputerCount -gt 1 } |
    Export-Csv -Path $DuplicatesCsv -NoTypeInformation -Encoding UTF8

# =========================
# ИТОГ НА ЭКРАН
# =========================

Write-Host ""
Write-Host "Готово." -ForegroundColor Green
Write-Host "Детальный отчёт: $DetailCsv"
Write-Host "Сводка лицензий: $SummaryCsv"
Write-Host "Дубли лицензий: $DuplicatesCsv"
Write-Host ""

Write-Host "Краткая сводка:" -ForegroundColor Yellow

$AllResults |
    Group-Object Product, Component, Status |
    Sort-Object Name |
    ForEach-Object {
        [PSCustomObject]@{
            Count     = $_.Count
            Product   = $_.Group[0].Product
            Component = $_.Group[0].Component
            Status    = $_.Group[0].Status
        }
    } |
    Format-Table -AutoSize