fix: Thumbprint при одном cert после нативного сброса (PS 5.1 unwrap)

Добавлены Get-RdpRepairCertificateArray/FirstCertificate, самотесты на вложенные
массивы; Fix-RdpTls больше не использует Select-Object -First 1 на cert.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-12 20:31:03 +10:00
parent 60b843b936
commit 5761710fa1
4 changed files with 61 additions and 8 deletions
+4 -3
View File
@@ -71,7 +71,7 @@ try {
Write-RdpRepairLog 'No cert in Remote Desktop store - run full repair or repair install first' 'ERROR' $logPath
exit 2
}
$valid = $certs | Where-Object { $_.NotAfter -gt (Get-Date) } | Select-Object -First 1
$valid = @(Get-RdpRepairCertificateArray $certs | Where-Object { $_.NotAfter -gt (Get-Date) })[0]
if (-not $valid) {
Write-RdpRepairLog 'Все cert просрочены' 'ERROR' $logPath
exit 2
@@ -98,7 +98,7 @@ try {
$snap = Get-RdpRepairDiagnosticSnapshot
$snap | Format-List ComputerName, OsCaption, SecurityLayer, SslHash, CertCount, Events1057_24h, GpoLicenseSrv
$expired = @($snap.Certificates | Where-Object { $_.NotAfter -lt (Get-Date) })
$expired = @(Get-RdpRepairCertificateArray $snap.Certificates | Where-Object { $_.NotAfter -lt (Get-Date) })
if ($expired.Count -gt 0) {
Write-RdpRepairLog "Найдено просроченных cert: $($expired.Count)" 'WARN' $logPath
}
@@ -111,7 +111,8 @@ try {
$native = Invoke-RdpRepairNativeReset -LogPath $logPath
if ($native.CertCount -gt 0) {
Write-RdpRepairLog "После сброса без reboot: нативный cert найден ($($native.CertCount))" 'OK' $logPath
$nativeCert = $native.Certificates | Select-Object -First 1
$nativeCert = Get-RdpRepairFirstCertificate -Certificates $native.Certificates
if (-not $nativeCert) { throw 'Native cert list empty after reset' }
$bindNative = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $nativeCert.Thumbprint -LogPath $logPath
if ($bindNative.Persisted) {
Write-RdpRepairLog 'Native cert accepted - you can use RestoreTls' 'OK' $logPath
+28
View File
@@ -76,6 +76,30 @@ Assert-Test 'Get-RdpRepairRemoteDesktopCertificates exposes Count' {
$null = $certs.Count
}
Assert-Test 'single cert in PSCustomObject keeps Thumbprint' {
$mock = [PSCustomObject]@{
Thumbprint = 'DEADBEEF'
Subject = 'CN=test'
NotAfter = (Get-Date).AddDays(30)
HasKey = $true
}
$box = [PSCustomObject]@{ Certificates = @($mock) }
$first = Get-RdpRepairFirstCertificate -Certificates $box.Certificates
if ($first.Thumbprint -ne 'DEADBEEF') { throw 'Thumbprint lost via property unwrap' }
}
Assert-Test 'Get-RdpRepairCertificateArray flattens nested Object[]' {
$mock = [PSCustomObject]@{
Thumbprint = 'NESTED01'
Subject = 'CN=nested'
NotAfter = (Get-Date).AddDays(1)
HasKey = $true
}
$box = [PSCustomObject]@{ Certificates = ,[object[]]@(,[object[]]@($mock)) }
$first = Get-RdpRepairFirstCertificate -Certificates $box.Certificates
if ($first.Thumbprint -ne 'NESTED01') { throw "expected NESTED01 got $($first.Thumbprint)" }
}
Assert-Test 'Get-RdpRepairDiagnosticSnapshot object shape' {
if (-not (Test-RdpRepairIsAdministrator)) {
Write-Host ' (skip snapshot fields - not admin)' -ForegroundColor DarkYellow
@@ -85,6 +109,10 @@ Assert-Test 'Get-RdpRepairDiagnosticSnapshot object shape' {
foreach ($prop in @('ComputerName', 'SecurityLayer', 'CertCount', 'TermService')) {
if (-not $s.PSObject.Properties[$prop]) { throw "missing property $prop" }
}
if ($s.CertCount -gt 0) {
$first = Get-RdpRepairFirstCertificate -Certificates $s.Certificates
if (-not $first.Thumbprint) { throw 'snapshot cert missing Thumbprint' }
}
}
Assert-Test 'Registry path constants well-formed' {
+27 -5
View File
@@ -86,8 +86,30 @@ function Get-RdpRepairRemoteDesktopCertificates {
} finally {
$store.Close()
}
# Запятая перед return: иначе PS 5.1 разворачивает массив из одного cert и ломает .Count
return ,[object[]]$list.ToArray()
return [object[]]$list.ToArray()
}
function Get-RdpRepairCertificateArray {
param([object]$Certificates)
if ($null -eq $Certificates) { return @() }
$flat = New-Object System.Collections.Generic.List[object]
foreach ($item in @($Certificates)) {
if ($item -is [object[]]) {
foreach ($inner in @($item)) {
if ($null -ne $inner) { [void]$flat.Add($inner) }
}
} elseif ($null -ne $item) {
[void]$flat.Add($item)
}
}
return [object[]]$flat.ToArray()
}
function Get-RdpRepairFirstCertificate {
param([object]$Certificates)
$arr = @(Get-RdpRepairCertificateArray -Certificates $Certificates)
if ($arr.Count -lt 1) { return $null }
return $arr[0]
}
function Clear-RdpRepairCertificateStores {
@@ -258,7 +280,7 @@ function Invoke-RdpRepairNativeReset {
return [PSCustomObject]@{
RebootScheduled = $false
CertCount = $certs.Count
Certificates = $certs
Certificates = ,[object[]]$certs
}
}
@@ -283,7 +305,7 @@ function Get-RdpRepairDiagnosticSnapshot {
TermService = (Get-Service TermService).Status
RdpAllowed = -not (Get-ItemProperty $script:RdpRepairTsKey).fDenyTSConnections
CertCount = $certs.Count
Certificates = $certs
Certificates = ,[object[]]$certs
GpoLicenseSrv = $pol.LicenseServers
GpoLicMode = $pol.LicensingMode
Events1057_24h = $ev1057
@@ -302,7 +324,7 @@ function Export-RdpRepairDiagnosticReport {
$snap.PSObject.Properties | ForEach-Object {
if ($_.Name -eq 'Certificates') {
$lines += 'Certificates:'
foreach ($c in $_.Value) {
foreach ($c in (Get-RdpRepairCertificateArray $_.Value)) {
$lines += (' - {0}; Thumb={1}; NotAfter={2}; Key={3}' -f $c.Subject, $c.Thumbprint, $c.NotAfter, $c.HasKey)
}
} else {