feat: локальные скрипты починки RDP/TLS для Windows (fix-DAK-PC)

Fix-RdpTls.ps1 с фазами диагностики, cert, ACL, привязкой; самотесты; docs repair install и возврат TLS.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
PTah
2026-06-12 19:05:29 +10:00
commit 14348a0181
9 changed files with 1055 additions and 0 deletions
+175
View File
@@ -0,0 +1,175 @@
#Requires -Version 5.1
#Requires -RunAsAdministrator
<#
.SYNOPSIS
Локальная починка RDP TLS / сертификата TermService.
.PARAMETER DiagnoseOnly
Только диагностика и отчёт.
.PARAMETER EmergencyRdpOnly
SecurityLayer=0 (без TLS) для срочного доступа.
.PARAMETER RestoreTls
Вернуть SecurityLayer=2 после успешной привязки cert.
.PARAMETER SkipReboot
Не предлагать reboot при нативном сбросе.
.PARAMETER NonInteractive
Без вопросов (для автоматизации). $env:RDP_REPAIR_NONINTERACTIVE=1
#>
[CmdletBinding()]
param(
[switch]$DiagnoseOnly,
[switch]$EmergencyRdpOnly,
[switch]$RestoreTls,
[switch]$SkipReboot,
[switch]$NonInteractive
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
if ($NonInteractive) { $env:RDP_REPAIR_NONINTERACTIVE = '1' }
$libDir = Join-Path $PSScriptRoot 'lib'
. (Join-Path $libDir 'RdpRepair.Common.ps1')
$logPath = Join-Path (Initialize-RdpRepairDataRoot) ("repair-{0:yyyyMMdd-HHmmss}.log" -f (Get-Date))
function Show-RdpRepairClientCacheHint {
Write-RdpRepairLog 'На клиенте RDP выполните:' 'INFO' $logPath
Write-RdpRepairLog ' Remove-Item "HKCU:\Software\Microsoft\Terminal Server Client\Servers\<имя-сервера>" -Recurse -Force' 'INFO' $logPath
}
try {
Assert-RdpRepairAdministrator
Write-RdpRepairLog "=== Fix-RdpTls start $(Get-Date -Format o) ===" 'STEP' $logPath
Write-RdpRepairLog "Компьютер: $env:COMPUTERNAME" 'INFO' $logPath
$report = Export-RdpRepairDiagnosticReport -LogPath $logPath
Write-RdpRepairLog "Диагностика сохранена: $report" 'OK' $logPath
if ($DiagnoseOnly) {
Get-RdpRepairDiagnosticSnapshot | Format-List
exit 0
}
if ($EmergencyRdpOnly) {
Write-RdpRepairLog 'ЭКСТРЕННЫЙ РЕЖИМ: SecurityLayer=0 (RDP без TLS)' 'WARN' $logPath
Set-RdpRepairSecurityLayer -Layer 0 -LogPath $logPath
Show-RdpRepairClientCacheHint
Write-RdpRepairLog 'Проверьте RDP. Для TLS после repair install: .\Fix-RdpTls.ps1 -RestoreTls' 'OK' $logPath
exit 0
}
if ($RestoreTls) {
Write-RdpRepairLog 'Возврат SecurityLayer=2 (TLS)' 'STEP' $logPath
$certs = Get-RdpRepairRemoteDesktopCertificates
if ($certs.Count -eq 0) {
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
if (-not $valid) {
Write-RdpRepairLog 'Все cert просрочены' 'ERROR' $logPath
exit 2
}
$thumb = $valid.Thumbprint
if (-not (Get-RdpRepairSslCertificateHash)) {
Write-RdpRepairLog "Привязка $($valid.Thumbprint)..." 'INFO' $logPath
Set-RdpRepairCertificateBinding -Thumbprint $thumb -LogPath $logPath | Out-Null
}
$bind = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $thumb -LogPath $logPath
if (-not $bind.Persisted) {
Write-RdpRepairLog 'TermService does not keep hash - cannot restore TLS. See docs\REPAIR-INSTALL.md' 'ERROR' $logPath
exit 3
}
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value 2
Restart-RdpRepairTermService
Write-RdpRepairLog 'SecurityLayer=2, TermService перезапущен' 'OK' $logPath
Show-RdpRepairClientCacheHint
exit 0
}
# --- Полный интерактивный ремонт ---
Write-RdpRepairLog 'ФАЗА 1: текущее состояние' 'STEP' $logPath
$snap = Get-RdpRepairDiagnosticSnapshot
$snap | Format-List ComputerName, OsCaption, SecurityLayer, SslHash, CertCount, Events1057_24h, GpoLicenseSrv
$expired = @($snap.Certificates | Where-Object { $_.NotAfter -lt (Get-Date) })
if ($expired.Count -gt 0) {
Write-RdpRepairLog "Найдено просроченных cert: $($expired.Count)" 'WARN' $logPath
}
if (-not (Confirm-RdpRepairContinue 'Продолжить: очистка cert и нативный сброс?')) {
exit 0
}
Write-RdpRepairLog 'ФАЗА 2: очистка stores + нативный сброс' 'STEP' $logPath
$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
$bindNative = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $nativeCert.Thumbprint -LogPath $logPath
if ($bindNative.Persisted) {
Write-RdpRepairLog 'Native cert accepted - you can use RestoreTls' 'OK' $logPath
if (Confirm-RdpRepairContinue 'Установить SecurityLayer=2 сейчас?') {
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value 2
Restart-RdpRepairTermService
}
Show-RdpRepairClientCacheHint
exit 0
}
}
if (-not $SkipReboot -and (Confirm-RdpRepairContinue 'Нативный cert не прижился. Запланировать REBOOT через 60 с?')) {
Invoke-RdpRepairNativeReset -LogPath $logPath -Reboot | Out-Null
Write-RdpRepairLog 'После загрузки снова запустите: .\Fix-RdpTls.ps1' 'WARN' $logPath
exit 0
}
if (-not (Confirm-RdpRepairContinue 'ФАЗА 3: создать локальный cert и привязать?')) {
exit 0
}
Write-RdpRepairLog 'ФАЗА 3: создание cert' 'STEP' $logPath
Clear-RdpRepairCertificateStores -LogPath $logPath
$dns = @(Get-RdpRepairComputerDnsNames)
Write-RdpRepairLog "DNS: $($dns -join ', ')" 'INFO' $logPath
$cert = New-RdpRepairSelfSignedCertificate -DnsNames $dns -LogPath $logPath
Write-RdpRepairLog 'ФАЗА 4: ACL + Remote Desktop store' 'STEP' $logPath
Set-RdpRepairPrivateKeyAcl -Certificate $cert -LogPath $logPath | Out-Null
Add-RdpRepairCertificateToRemoteDesktopStore -Certificate $cert
Write-RdpRepairLog 'ФАЗА 5: привязка и проверка AFTER' 'STEP' $logPath
Set-RdpRepairCertificateBinding -Thumbprint $cert.Thumbprint -LogPath $logPath | Out-Null
$bind = Test-RdpRepairCertificateBindingPersisted -ExpectedThumbprint $cert.Thumbprint -LogPath $logPath
if ($bind.Persisted) {
if (Confirm-RdpRepairContinue 'Привязка OK. Установить SecurityLayer=2?') {
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value 2
Restart-RdpRepairTermService
}
Show-RdpRepairClientCacheHint
Write-RdpRepairLog '=== Ремонт TLS завершён успешно ===' 'OK' $logPath
exit 0
}
Write-RdpRepairLog '=== TermService отклоняет cert ===' 'ERROR' $logPath
Write-RdpRepairLog 'Варианты:' 'WARN' $logPath
Write-RdpRepairLog ' 1) docs\REPAIR-INSTALL.md - repair install Windows' 'WARN' $logPath
Write-RdpRepairLog ' 2) .\Fix-RdpTls.ps1 -EmergencyRdpOnly - RDP without TLS' 'WARN' $logPath
if (Confirm-RdpRepairContinue 'Включить Emergency SecurityLayer=0 сейчас?') {
Set-RdpRepairSecurityLayer -Layer 0 -LogPath $logPath
Show-RdpRepairClientCacheHint
}
exit 4
} catch {
Write-RdpRepairLog "FATAL: $($_.Exception.Message)" 'ERROR' $logPath
Write-RdpRepairLog $_.ScriptStackTrace 'ERROR' $logPath
exit 1
}
+138
View File
@@ -0,0 +1,138 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Самотесты модуля RdpRepair (синтаксис, функции, dry-run логики).
#>
[CmdletBinding()]
param(
[switch]$IncludeAdminTests
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$script:SelfTestFailed = 0
$script:SelfTestPassed = 0
function Assert-Test {
param(
[string]$Name,
[scriptblock]$Test
)
try {
& $Test
$script:SelfTestPassed++
Write-Host "[PASS] $Name" -ForegroundColor Green
} catch {
$script:SelfTestFailed++
Write-Host "[FAIL] $Name - $($_.Exception.Message)" -ForegroundColor Red
}
}
$libPath = Join-Path $PSScriptRoot 'lib\RdpRepair.Common.ps1'
$mainPath = Join-Path $PSScriptRoot 'Fix-RdpTls.ps1'
. $libPath
Write-Host "`n=== RdpRepair SelfTest ===" -ForegroundColor Cyan
Assert-Test 'RdpRepair.Common.ps1 exists' {
if (-not (Test-Path -LiteralPath $libPath)) { throw 'file missing' }
}
Assert-Test 'Fix-RdpTls.ps1 exists' {
if (-not (Test-Path -LiteralPath $mainPath)) { throw 'file missing' }
}
Assert-Test 'PowerShell parse RdpRepair.Common.ps1' {
$parseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile($libPath, [ref]$null, [ref]$parseErrors)
if ($parseErrors.Count -gt 0) { throw (($parseErrors | ForEach-Object { $_.ToString() }) -join '; ') }
}
Assert-Test 'PowerShell parse Fix-RdpTls.ps1' {
$parseErrors = $null
$null = [System.Management.Automation.Language.Parser]::ParseFile($mainPath, [ref]$null, [ref]$parseErrors)
if ($parseErrors.Count -gt 0) { throw (($parseErrors | ForEach-Object { $_.ToString() }) -join '; ') }
}
Assert-Test 'Dot-source Common without throw' {
if (-not (Get-Command Initialize-RdpRepairDataRoot -ErrorAction SilentlyContinue)) {
throw 'Common module not loaded'
}
}
Assert-Test 'Initialize-RdpRepairDataRoot returns path' {
$p = Initialize-RdpRepairDataRoot
if (-not (Test-Path -LiteralPath $p)) { throw 'dir not created' }
}
Assert-Test 'Get-RdpRepairComputerDnsNames non-empty' {
$n = Get-RdpRepairComputerDnsNames
if ($n.Count -lt 1) { throw 'no dns names' }
}
Assert-Test 'Get-RdpRepairDiagnosticSnapshot object shape' {
if (-not (Test-RdpRepairIsAdministrator)) {
Write-Host ' (skip snapshot fields - not admin)' -ForegroundColor DarkYellow
return
}
$s = Get-RdpRepairDiagnosticSnapshot
foreach ($prop in @('ComputerName', 'SecurityLayer', 'CertCount', 'TermService')) {
if (-not $s.PSObject.Properties[$prop]) { throw "missing property $prop" }
}
}
Assert-Test 'Registry path constants well-formed' {
if ($script:RdpRepairRdpTcpKey -notmatch 'RDP-Tcp$') { throw 'bad rdp tcp key' }
}
Assert-Test 'Export-RdpRepairDiagnosticReport writes file' {
if (-not (Test-RdpRepairIsAdministrator)) {
Write-Host ' (skip report write - not admin)' -ForegroundColor DarkYellow
return
}
$log = Join-Path $env:TEMP 'rdp-selftest.log'
$rp = Export-RdpRepairDiagnosticReport -LogPath $log
if (-not (Test-Path -LiteralPath $rp)) { throw 'report not written' }
if ((Get-Item $rp).Length -lt 50) { throw 'report too small' }
}
Assert-Test 'Confirm-RdpRepairContinue noninteractive' {
$env:RDP_REPAIR_NONINTERACTIVE = '1'
if (-not (Confirm-RdpRepairContinue 'test')) { throw 'expected true' }
Remove-Item Env:\RDP_REPAIR_NONINTERACTIVE -ErrorAction SilentlyContinue
}
Assert-Test 'SecurityLayer values documented (0,1,2)' {
foreach ($v in 0, 1, 2) {
if ($v -lt 0 -or $v -gt 2) { throw 'invalid' }
}
}
Assert-Test 'docs REPAIR-INSTALL.md exists' {
$d = Join-Path (Split-Path $PSScriptRoot -Parent) 'docs\REPAIR-INSTALL.md'
if (-not (Test-Path -LiteralPath $d)) { throw 'missing' }
}
Assert-Test 'docs RETURN-TO-TLS.md exists' {
$d = Join-Path (Split-Path $PSScriptRoot -Parent) 'docs\RETURN-TO-TLS.md'
if (-not (Test-Path -LiteralPath $d)) { throw 'missing' }
}
if ($IncludeAdminTests -and (Test-RdpRepairIsAdministrator)) {
Write-Host "`n--- Admin-only tests ---" -ForegroundColor Cyan
Assert-Test 'TermService service exists' {
if (-not (Get-Service TermService -ErrorAction SilentlyContinue)) { throw 'TermService missing' }
}
Assert-Test 'Get-RdpRepairRemoteDesktopCertificates no throw' {
$null = Get-RdpRepairRemoteDesktopCertificates
}
} elseif ($IncludeAdminTests) {
Write-Host "`n(Warning: -IncludeAdminTests skipped - not running as Administrator)" -ForegroundColor Yellow
}
Write-Host "`n=== Results: PASS=$script:SelfTestPassed FAIL=$script:SelfTestFailed ===" -ForegroundColor Cyan
if ($script:SelfTestFailed -gt 0) {
exit 1
}
exit 0
+336
View File
@@ -0,0 +1,336 @@
#Requires -Version 5.1
Set-StrictMode -Version Latest
$script:RdpRepairDataRoot = Join-Path $env:ProgramData 'RdpRepair'
$script:RdpRepairRdpTcpKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
$script:RdpRepairTsKey = 'HKLM:\SYSTEM\CurrentControlSet\Control\Terminal Server'
function Initialize-RdpRepairDataRoot {
if (-not (Test-Path -LiteralPath $script:RdpRepairDataRoot)) {
New-Item -ItemType Directory -Path $script:RdpRepairDataRoot -Force | Out-Null
}
return $script:RdpRepairDataRoot
}
function Write-RdpRepairLog {
param(
[string]$Message,
[ValidateSet('INFO', 'WARN', 'ERROR', 'OK', 'STEP')]
[string]$Level = 'INFO',
[string]$LogPath
)
$ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$line = "[$ts] [$Level] $Message"
if ($LogPath) {
Add-Content -LiteralPath $LogPath -Value $line -Encoding UTF8
}
switch ($Level) {
'ERROR' { Write-Host $line -ForegroundColor Red }
'WARN' { Write-Host $line -ForegroundColor Yellow }
'OK' { Write-Host $line -ForegroundColor Green }
'STEP' { Write-Host $line -ForegroundColor Cyan }
default { Write-Host $line }
}
}
function Test-RdpRepairIsAdministrator {
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
$p = New-Object Security.Principal.WindowsPrincipal($id)
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Assert-RdpRepairAdministrator {
if (-not (Test-RdpRepairIsAdministrator)) {
throw 'Run PowerShell as Administrator.'
}
}
function Get-RdpRepairComputerDnsNames {
$cs = Get-CimInstance Win32_ComputerSystem -ErrorAction Stop
$computer = $env:COMPUTERNAME
$fqdn = "$computer.$($cs.Domain)".ToLower()
$names = @($computer, $fqdn)
if ($fqdn -notmatch '\.') {
$names += "$computer.local"
}
return $names | Select-Object -Unique
}
function Get-RdpRepairSslCertificateHash {
$p = Get-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SSLCertificateSHA1Hash -ErrorAction SilentlyContinue
if ($null -eq $p -or [string]::IsNullOrWhiteSpace($p.SSLCertificateSHA1Hash)) {
return $null
}
return $p.SSLCertificateSHA1Hash
}
function Get-RdpRepairSecurityLayer {
$p = Get-ItemProperty -Path $script:RdpRepairRdpTcpKey -ErrorAction SilentlyContinue
if ($null -eq $p.SecurityLayer) { return '(default)' }
return [int]$p.SecurityLayer
}
function Get-RdpRepairRemoteDesktopCertificates {
$list = New-Object System.Collections.Generic.List[object]
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Remote Desktop', 'LocalMachine')
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)
try {
foreach ($c in $store.Certificates) {
$list.Add([PSCustomObject]@{
Subject = $c.Subject
Thumbprint = $c.Thumbprint
NotAfter = $c.NotAfter
HasKey = $c.HasPrivateKey
})
}
} finally {
$store.Close()
}
return $list
}
function Clear-RdpRepairCertificateStores {
param([string]$LogPath)
foreach ($sn in @('Remote Desktop', 'My')) {
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($sn, 'LocalMachine')
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
try {
foreach ($c in @($store.Certificates)) {
Write-RdpRepairLog "Удаление cert из ${sn}: $($c.Thumbprint) ($($c.Subject))" 'INFO' $LogPath
$store.Remove($c)
}
} finally {
$store.Close()
}
}
Remove-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SSLCertificateSHA1Hash -ErrorAction SilentlyContinue
}
function Get-RdpRepairPrivateKeyPath {
param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate)
$rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate)
if (-not $rsa) { return $null }
$name = $rsa.Key.UniqueName
foreach ($base in @(
Join-Path $env:ProgramData 'Microsoft\Crypto\RSA\MachineKeys'
Join-Path $env:ProgramData 'Microsoft\Crypto\Keys'
)) {
$path = Join-Path $base $name
if (Test-Path -LiteralPath $path) { return $path }
}
return $null
}
function Set-RdpRepairPrivateKeyAcl {
param(
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
[string]$LogPath
)
$keyPath = Get-RdpRepairPrivateKeyPath -Certificate $Certificate
if (-not $keyPath) {
Write-RdpRepairLog 'Не найден файл приватного ключа' 'WARN' $LogPath
return $false
}
& icacls.exe $keyPath /grant 'NETWORK SERVICE:(F)' 'NT AUTHORITY\SYSTEM:(F)' 'BUILTIN\Administrators:(F)' 'NT AUTHORITY\LOCAL SERVICE:(R)' /c | Out-Null
Write-RdpRepairLog "ACL обновлён: $keyPath" 'OK' $LogPath
return $true
}
function Add-RdpRepairCertificateToRemoteDesktopStore {
param([System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate)
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store('Remote Desktop', 'LocalMachine')
$store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
try {
$store.Add($Certificate)
} finally {
$store.Close()
}
}
function Set-RdpRepairCertificateBinding {
param(
[string]$Thumbprint,
[string]$LogPath
)
$thumb = $Thumbprint.ToUpper()
$regPath = 'HKLM\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp'
& reg.exe add $regPath /v SSLCertificateSHA1Hash /t REG_SZ /d $thumb /f | Out-Null
try {
$ts = Get-CimInstance -Namespace root\cimv2\terminalservices -ClassName Win32_TSGeneralSetting -Filter "TerminalName='RDP-Tcp'" -ErrorAction Stop
$ts | Set-CimInstance -Property @{ SSLCertificateSHA1Hash = $thumb } -ErrorAction Stop
Write-RdpRepairLog "WMI: SSLCertificateSHA1Hash = $thumb" 'INFO' $LogPath
} catch {
Write-RdpRepairLog "WMI Set-CimInstance: $($_.Exception.Message) (reg уже записан)" 'WARN' $LogPath
}
return (Get-RdpRepairSslCertificateHash)
}
function Restart-RdpRepairTermService {
param([int]$WaitSeconds = 8)
Stop-Service TermService -Force -ErrorAction Stop
Start-Sleep -Seconds 2
Start-Service TermService -ErrorAction Stop
Start-Sleep -Seconds $WaitSeconds
}
function Test-RdpRepairCertificateBindingPersisted {
param(
[string]$ExpectedThumbprint,
[string]$LogPath
)
$before = Get-RdpRepairSslCertificateHash
Write-RdpRepairLog "Hash BEFORE TermService start: $(if ($before) { $before } else { '(empty)' })" 'INFO' $LogPath
Restart-RdpRepairTermService
$after = Get-RdpRepairSslCertificateHash
Write-RdpRepairLog "Hash AFTER TermService start: $(if ($after) { $after } else { '(empty)' })" 'INFO' $LogPath
$ok = ($after -eq $ExpectedThumbprint.ToUpper())
if ($ok) {
Write-RdpRepairLog 'Привязка cert принята TermService' 'OK' $LogPath
} else {
Write-RdpRepairLog 'TermService сбросил или отклонил привязку cert' 'ERROR' $LogPath
}
return [PSCustomObject]@{
Before = $before
After = $after
Persisted = $ok
}
}
function New-RdpRepairSelfSignedCertificate {
param(
[string[]]$DnsNames,
[string]$LogPath
)
$providers = @(
@{ Name = 'Microsoft Enhanced RSA and AES Cryptographic Provider'; KeySpec = 'KeyExchange' }
@{ Name = $null; KeySpec = $null }
)
$notAfter = (Get-Date).AddYears(2)
$subject = "CN=$($DnsNames | Select-Object -First 1)"
foreach ($prov in $providers) {
try {
$params = @{
Subject = $subject
DnsName = $DnsNames
CertStoreLocation = 'Cert:\LocalMachine\My'
KeyAlgorithm = 'RSA'
KeyLength = 2048
KeyUsage = 'DigitalSignature', 'KeyEncipherment'
TextExtension = @('2.5.29.37={text}1.3.6.1.5.5.7.3.1')
NotAfter = $notAfter
}
if ($prov.Name) {
$params['Provider'] = $prov.Name
$params['KeySpec'] = $prov.KeySpec
}
$cert = New-SelfSignedCertificate @params
$keyPath = Get-RdpRepairPrivateKeyPath -Certificate $cert
$provLabel = if ($prov.Name) { $prov.Name } else { 'default' }
Write-RdpRepairLog "Создан cert $($cert.Thumbprint) провайдер=$provLabel key=$keyPath" 'OK' $LogPath
return $cert
} catch {
$provLabel = if ($prov.Name) { $prov.Name } else { 'default' }
Write-RdpRepairLog "New-SelfSignedCertificate ($provLabel): $($_.Exception.Message)" 'WARN' $LogPath
}
}
throw 'Не удалось создать самозаверяющий сертификат ни одним провайдером.'
}
function Invoke-RdpRepairNativeReset {
param(
[string]$LogPath,
[switch]$Reboot
)
Write-RdpRepairLog 'Нативный сброс RDP cert (без ручного cert)' 'STEP' $LogPath
Stop-Service TermService -Force -ErrorAction SilentlyContinue
Set-ItemProperty -Path $script:RdpRepairTsKey -Name fDenyTSConnections -Value 1
Clear-RdpRepairCertificateStores -LogPath $LogPath
Set-ItemProperty -Path $script:RdpRepairTsKey -Name fDenyTSConnections -Value 0
if ($Reboot) {
Write-RdpRepairLog 'Перезагрузка через 60 с для нативного создания cert...' 'WARN' $LogPath
shutdown.exe /r /t 60 /f /c 'RdpRepair: native certificate rebuild'
return [PSCustomObject]@{ RebootScheduled = $true }
}
Start-Service TermService -ErrorAction SilentlyContinue
Start-Sleep -Seconds 5
$certs = Get-RdpRepairRemoteDesktopCertificates
return [PSCustomObject]@{
RebootScheduled = $false
CertCount = $certs.Count
Certificates = $certs
}
}
function Get-RdpRepairDiagnosticSnapshot {
$polPath = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services'
$pol = if (Test-Path $polPath) { Get-ItemProperty $polPath -ErrorAction SilentlyContinue } else { $null }
$fips = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\FipsAlgorithmPolicy' -Name Enabled -ErrorAction SilentlyContinue).Enabled
$certs = Get-RdpRepairRemoteDesktopCertificates
$ev1057 = @(Get-WinEvent -FilterHashtable @{
LogName = 'System'
Id = 1057, 36870
StartTime = (Get-Date).AddHours(-24)
} -ErrorAction SilentlyContinue).Count
$os = Get-CimInstance Win32_OperatingSystem
return [PSCustomObject]@{
ComputerName = $env:COMPUTERNAME
OsCaption = $os.Caption
OsBuild = $os.BuildNumber
FipsEnabled = $fips
SecurityLayer = Get-RdpRepairSecurityLayer
SslHash = Get-RdpRepairSslCertificateHash
TermService = (Get-Service TermService).Status
RdpAllowed = -not (Get-ItemProperty $script:RdpRepairTsKey).fDenyTSConnections
CertCount = $certs.Count
Certificates = $certs
GpoLicenseSrv = $pol.LicenseServers
GpoLicMode = $pol.LicensingMode
Events1057_24h = $ev1057
}
}
function Export-RdpRepairDiagnosticReport {
param([string]$LogPath)
$snap = Get-RdpRepairDiagnosticSnapshot
$reportPath = Join-Path (Initialize-RdpRepairDataRoot) ("report-{0:yyyyMMdd-HHmmss}.txt" -f (Get-Date))
$lines = @(
"=== RDP/TLS Diagnostic Report ==="
"Generated: $(Get-Date -Format o)"
""
)
$snap.PSObject.Properties | ForEach-Object {
if ($_.Name -eq 'Certificates') {
$lines += 'Certificates:'
foreach ($c in $_.Value) {
$lines += (' - {0}; Thumb={1}; NotAfter={2}; Key={3}' -f $c.Subject, $c.Thumbprint, $c.NotAfter, $c.HasKey)
}
} else {
$lines += ('{0}: {1}' -f $_.Name, $_.Value)
}
}
$text = $lines -join "`r`n"
[System.IO.File]::WriteAllText($reportPath, $text, [System.Text.UTF8Encoding]::new($false))
Write-RdpRepairLog "Отчёт: $reportPath" 'OK' $LogPath
return $reportPath
}
function Set-RdpRepairSecurityLayer {
param(
[int]$Layer,
[string]$LogPath
)
Stop-Service TermService -Force -ErrorAction Stop
Set-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SecurityLayer -Value $Layer
if ($Layer -eq 0 -or $Layer -eq 1) {
Remove-ItemProperty -Path $script:RdpRepairRdpTcpKey -Name SSLCertificateSHA1Hash -ErrorAction SilentlyContinue
}
Start-Service TermService -ErrorAction Stop
Write-RdpRepairLog "SecurityLayer установлен в $Layer" 'OK' $LogPath
}
function Confirm-RdpRepairContinue {
param([string]$Prompt)
if ($env:RDP_REPAIR_NONINTERACTIVE -eq '1') { return $true }
$r = Read-Host "$Prompt [Y/n]"
return ($r -eq '' -or $r -match '^[yY]$')
}