Загрузка данных
#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