diff --git a/Deploy-LoginMonitor.ps1 b/Deploy-LoginMonitor.ps1 index efb47b0..1b6d50a 100644 --- a/Deploy-LoginMonitor.ps1 +++ b/Deploy-LoginMonitor.ps1 @@ -9,6 +9,8 @@ СТРУКТУРА НА ШАРЕ (пример): \\dc\share\RDP-login-monitor\Login_Monitor.ps1 + \\dc\share\RDP-login-monitor\Sac-Client.ps1 — обязателен для SAC + \\dc\share\RDP-login-monitor\login_monitor.settings.example.ps1 \\dc\share\RDP-login-monitor\version.txt — одна строка, например: 1.3.0 \\dc\share\RDP-login-monitor\Deploy-LoginMonitor.ps1 @@ -51,7 +53,9 @@ $VersionStampPath = Join-Path $InstallRoot "deployed_version.txt" $DeployUpdateMarkerPath = Join-Path $InstallRoot "deploy_last_update.txt" $DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log" $ScriptName = "Login_Monitor.ps1" +$SacClientName = "Sac-Client.ps1" $VersionFileName = "version.txt" +$DeployBundleFiles = @($ScriptName, $SacClientName) $PsExe = "$env:SystemRoot\System32\WindowsPowerShell\v1.0\powershell.exe" $Utf8Bom = New-Object System.Text.UTF8Encoding $true @@ -194,13 +198,49 @@ function Get-RdpMonitorSettingsRaw { } } -function Test-RdpMonitorSettingsNeedsSacBootstrap { - param( - [string]$SettingsPath, - [string]$SacClientPath - ) - if (-not (Test-Path -LiteralPath $SacClientPath)) { return $true } +function Get-DeployFileSha256 { + param([string]$Path) + if (-not (Test-Path -LiteralPath $Path)) { return $null } + return (Get-FileHash -LiteralPath $Path -Algorithm SHA256 -ErrorAction Stop).Hash +} +function Test-RdpMonitorDeployBundleNeedsSync { + param([string]$ShareRoot) + foreach ($rel in $DeployBundleFiles) { + $src = Join-Path $ShareRoot $rel + if (-not (Test-Path -LiteralPath $src)) { + if ($rel -eq $SacClientName) { + Write-DeployLog "ОШИБКА: на шаре нет обязательного $SacClientName — опубликуйте пакет (update-rdp-monitor.ps1)." + } + continue + } + $dst = Join-Path $InstallRoot $rel + if (-not (Test-Path -LiteralPath $dst)) { return $true } + $hs = Get-DeployFileSha256 -Path $src + $hd = Get-DeployFileSha256 -Path $dst + if ($hs -ne $hd) { return $true } + } + return $false +} + +function Copy-RdpMonitorDeployBundle { + param([string]$ShareRoot) + foreach ($rel in $DeployBundleFiles) { + $src = Join-Path $ShareRoot $rel + $dst = Join-Path $InstallRoot $rel + if (-not (Test-Path -LiteralPath $src)) { + if ($rel -eq $SacClientName) { + Write-DeployLog "Предупреждение: на шаре нет $SacClientName — SAC недоступен до публикации файла на шару." + } + continue + } + Copy-Item -LiteralPath $src -Destination $dst -Force + Write-DeployLog "Файл скопирован: $dst" + } +} + +function Test-RdpMonitorSettingsNeedsSacBootstrap { + param([string]$SettingsPath) $c = Get-RdpMonitorSettingsRaw -Path $SettingsPath if ([string]::IsNullOrWhiteSpace($c)) { return $true } @@ -210,10 +250,112 @@ function Test-RdpMonitorSettingsNeedsSacBootstrap { } if ($c -match '(?m)^\s*\$SacApiKey\s*=\s*[''"]\s*[''"]') { return $true } if ($c -notmatch '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_[^''"]+[''"]') { return $true } + if ($c -notmatch '(?m)^\s*\$SacUrl\s*=\s*[''"]https?://[^''"]+[''"]') { return $true } return $false } +function Test-RdpMonitorSettingsNeedsServerDisplayNameHint { + param([string]$SettingsPath) + if (-not (Test-Path -LiteralPath $SettingsPath)) { return $false } + $c = Get-RdpMonitorSettingsRaw -Path $SettingsPath + if ([string]::IsNullOrWhiteSpace($c)) { return $false } + if ($c -match '(?m)^\s*(\#\s*)?\$ServerDisplayName\s*=') { return $false } + return $true +} + +function Update-RdpMonitorSettingsServerDisplayNameHintIfMissing { + param([string]$LocalSettings) + if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false } + if (-not (Test-RdpMonitorSettingsNeedsServerDisplayNameHint -SettingsPath $LocalSettings)) { + return $false + } + + $c = Get-RdpMonitorSettingsRaw -Path $LocalSettings + if ([string]::IsNullOrWhiteSpace($c)) { return $false } + + $hostLabel = if ([string]::IsNullOrWhiteSpace($env:COMPUTERNAME)) { 'New-PC' } else { $env:COMPUTERNAME.Trim() } + $hintBlock = @( + '# --- Подпись сервера в Telegram и SAC (host.display_name); раскомментируйте при необходимости ---' + "# `$ServerDisplayName = '$hostLabel'" + '# --- Явный IPv4 хоста для SAC (опционально; иначе автоопределение) ---' + '# $ServerIPv4 = ''' + ) -join "`r`n" + + $bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')" + Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force + Write-DeployLog "Добавление закомментированного `$ServerDisplayName в login_monitor.settings.ps1; резервная копия: $bak" + + $insertBefore = '(?m)^\s*#\s*---\s*Security Alert Center' + if ($c -match $insertBefore) { + $newContent = [regex]::Replace($c, $insertBefore, ($hintBlock + "`r`n`r`n" + '$0'), 1) + } else { + $newContent = ($c.TrimEnd() + "`r`n`r`n" + $hintBlock + "`r`n") + } + [System.IO.File]::WriteAllText($LocalSettings, $newContent.TrimEnd() + "`r`n", $Utf8Bom) + Write-DeployLog "login_monitor.settings.ps1: добавлена подсказка # `$ServerDisplayName = '$hostLabel'" + return $true +} + +function Get-RdpMonitorSacBlockFromExample { + param([string]$ExamplePath) + if (-not (Test-Path -LiteralPath $ExamplePath)) { return $null } + $ex = Get-RdpMonitorSettingsRaw -Path $ExamplePath + if ([string]::IsNullOrWhiteSpace($ex)) { return $null } + if ($ex -match '(?ms)(#\s*---\s*Security Alert Center.*?)(?=\r?\n#\s*---|\z)') { + return $Matches[1].TrimEnd() + } + return @( + '# --- Security Alert Center (SAC) ---' + '$UseSAC = ''dual''' + '$SacUrl = ''https://sac.kalinamall.ru''' + '$SacApiKey = ''sac_CHANGE_ME''' + ) -join "`r`n" +} + +function Update-RdpMonitorSettingsSacBlockIfMissing { + param( + [string]$LocalSettings, + [string]$ExampleOnShare + ) + if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false } + + $c = Get-RdpMonitorSettingsRaw -Path $LocalSettings + if ([string]::IsNullOrWhiteSpace($c)) { return $false } + + if ($c -match '(?m)^\s*\$UseSAC\s*=' -and $c -match '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_[^''"]+[''"]') { + if ($c -notmatch '(?m)^\s*\$UseSAC\s*=\s*[''"]off[''"]') { + Write-DeployLog "login_monitor.settings.ps1: блок SAC уже задан, файл не меняем." + return $false + } + } + + $sacBlock = Get-RdpMonitorSacBlockFromExample -ExamplePath $ExampleOnShare + if ([string]::IsNullOrWhiteSpace($sacBlock)) { + Write-DeployLog "Предупреждение: не удалось извлечь блок SAC из example — пропуск patch settings." + return $false + } + + $bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')" + Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force + Write-DeployLog "Добавление блока SAC (UseSAC=dual) в login_monitor.settings.ps1; резервная копия: $bak" + + if ($c -match '(?m)^\s*\$UseSAC\s*=') { + $c = [regex]::Replace($c, '(?m)^\s*\$UseSAC\s*=.*$', '') + $c = [regex]::Replace($c, '(?m)^\s*\$SacUrl\s*=.*$', '') + $c = [regex]::Replace($c, '(?m)^\s*\$SacApiKey\s*=.*$', '') + $c = [regex]::Replace($c, '(?m)^\s*\$SacSpoolDir\s*=.*$', '') + $c = [regex]::Replace($c, '(?m)^\s*\$SacTimeoutSec\s*=.*$', '') + $c = [regex]::Replace($c, '(?m)^\s*\$SacTlsSkipVerify\s*=.*$', '') + $c = [regex]::Replace($c, '(?m)^\s*\$SacFallbackFailures\s*=.*$', '') + } + + $newContent = ($c.TrimEnd() + "`r`n`r`n" + $sacBlock + "`r`n").TrimEnd() + "`r`n" + [System.IO.File]::WriteAllText($LocalSettings, $newContent, $Utf8Bom) + Write-DeployLog "login_monitor.settings.ps1: добавлен блок SAC (UseSAC=dual из example на шаре)." + return $true +} + function Sync-RdpMonitorSettingsFromShare { param( [string]$ExampleOnShare, @@ -224,12 +366,12 @@ function Sync-RdpMonitorSettingsFromShare { return } - $localSac = Join-Path $InstallRoot 'Sac-Client.ps1' $needsCreate = -not (Test-Path -LiteralPath $LocalSettings) - $needsBootstrap = $needsCreate -or (Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $LocalSettings -SacClientPath $localSac) + $needsBootstrap = $needsCreate -or (Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $LocalSettings) if (-not $needsBootstrap) { Write-DeployLog "login_monitor.settings.ps1: SAC уже настроен, файл не перезаписываем." + Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings | Out-Null return } @@ -239,13 +381,21 @@ function Sync-RdpMonitorSettingsFromShare { if ($needsCreate) { Write-DeployLog "Создан login_monitor.settings.ps1 из example (Telegram + SAC dual)." - } else { - $bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')" - Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force - Write-DeployLog "Апгрейд с версии без SAC: резервная копия $bak, применяем example с шары." + Copy-Item -LiteralPath $ExampleOnShare -Destination $LocalSettings -Force + Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings | Out-Null + return } + if (Update-RdpMonitorSettingsSacBlockIfMissing -LocalSettings $LocalSettings -ExampleOnShare $ExampleOnShare) { + Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings | Out-Null + return + } + + $bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')" + Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force + Write-DeployLog "Апгрейд settings: резервная копия $bak, применяем example с шары." Copy-Item -LiteralPath $ExampleOnShare -Destination $LocalSettings -Force + Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings | Out-Null } function Stop-RdpLoginMonitorMainProcesses { @@ -317,10 +467,14 @@ try { $localVerRaw = Get-LocalDeployedVersion Write-DeployLog "Версия на шаре: $shareVerRaw; локально установлено: $(if ($localVerRaw) { $localVerRaw } else { '(нет)' })." - $localSacPath = Join-Path $InstallRoot 'Sac-Client.ps1' - $sourceSacOnShare = Join-Path $shareRoot 'Sac-Client.ps1' - $needsSacBootstrap = (Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $settingsLocal -SacClientPath $localSacPath) ` - -or ((-not (Test-Path -LiteralPath $localSacPath)) -and (Test-Path -LiteralPath $sourceSacOnShare)) + if (-not (Test-Path -LiteralPath (Join-Path $shareRoot $SacClientName))) { + Write-DeployLog "ОШИБКА: на шаре отсутствует $SacClientName — выполните update-rdp-monitor.ps1 на сервере публикации." + } + + $needsSettingsBootstrap = Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $settingsLocal + $needsDisplayNameHint = Test-RdpMonitorSettingsNeedsServerDisplayNameHint -SettingsPath $settingsLocal + $needsBundleSync = Test-RdpMonitorDeployBundleNeedsSync -ShareRoot $shareRoot + $needsSacBootstrap = $needsSettingsBootstrap -or $needsBundleSync -or $needsDisplayNameHint $cmp = Compare-VersionStrings -Left $shareVerRaw -Right $localVerRaw if ($null -ne $cmp) { @@ -329,7 +483,13 @@ try { Write-DeployLog "Актуально, копирование не требуется." exit 0 } - Write-DeployLog "Версия совпадает ($shareVerRaw), но нужна донастройка SAC — продолжаем деплой." + if ($needsBundleSync) { + Write-DeployLog "Версия совпадает ($shareVerRaw), но пакет файлов (Login_Monitor/Sac-Client) отличается — продолжаем деплой." + } elseif ($needsSettingsBootstrap) { + Write-DeployLog "Версия совпадает ($shareVerRaw), но нужна донастройка SAC в settings — продолжаем деплой." + } elseif ($needsDisplayNameHint) { + Write-DeployLog "Версия совпадает ($shareVerRaw), но нет подсказки `$ServerDisplayName в settings — продолжаем деплой." + } } if ($cmp -lt 0 -and -not $AllowDowngrade) { Write-DeployLog "На шаре версия старше локальной — пропуск (используйте -AllowDowngrade для отката)." @@ -346,10 +506,13 @@ try { } if ($WhatIf) { - if ($needsSacBootstrap) { - Write-DeployLog "[WhatIf] Применить login_monitor.settings.ps1 из example (SAC/Telegram)." + if ($needsSettingsBootstrap) { + Write-DeployLog "[WhatIf] Донастроить login_monitor.settings.ps1 (SAC dual из example)." } - Write-DeployLog "[WhatIf] Скопировать $sourceScript -> $LocalScript; InstallTasks; версия $shareVerRaw" + if ($needsBundleSync) { + Write-DeployLog "[WhatIf] Синхронизировать пакет: $($DeployBundleFiles -join ', ')." + } + Write-DeployLog "[WhatIf] InstallTasks; версия $shareVerRaw" exit 0 } @@ -360,17 +523,7 @@ try { Stop-RdpLoginMonitorMainProcesses - Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force - Write-DeployLog "Файл скопирован: $LocalScript" - - $sourceSac = Join-Path $shareRoot 'Sac-Client.ps1' - $localSac = Join-Path $InstallRoot 'Sac-Client.ps1' - if (Test-Path -LiteralPath $sourceSac) { - Copy-Item -LiteralPath $sourceSac -Destination $localSac -Force - Write-DeployLog "Файл скопирован: $localSac" - } else { - Write-DeployLog "Предупреждение: на шаре нет Sac-Client.ps1 — SAC будет недоступен до следующего деплоя." - } + Copy-RdpMonitorDeployBundle -ShareRoot $shareRoot Sync-RdpMonitorSettingsFromShare -ExampleOnShare $settingsExampleShare -LocalSettings $settingsLocal diff --git a/Docs/deploy-rdp-login-monitor.md b/Docs/deploy-rdp-login-monitor.md index 6359913..85ad15a 100644 --- a/Docs/deploy-rdp-login-monitor.md +++ b/Docs/deploy-rdp-login-monitor.md @@ -30,9 +30,11 @@ 2. Читается **`version.txt`** на шаре и сравнивается с локальной меткой **`deployed_version.txt`**. Если метки нет — подтягивается **`$ScriptVersion`** из установленного **`Login_Monitor.ps1`**. -3. Версия на шаре **совпадает** с локальной — выход без копирования. +3. Версия на шаре **совпадает** с локальной — выход **без копирования**, **кроме** случаев: + - на ПК **нет** `Sac-Client.ps1` или его SHA256 **отличается** от шары; + - в `login_monitor.settings.ps1` **нет** блока SAC (`$UseSAC`, `$SacUrl`, `$SacApiKey`) — тогда добавляется **`UseSAC = 'dual'`** из example (без перезаписи Telegram, если возможно). -4. Версия на шаре **новее** — остановка процессов монитора → копирование **`Login_Monitor.ps1`** и **`Sac-Client.ps1`** → **`Login_Monitor.ps1 -InstallTasks`** → **`deployed_version.txt`** → запуск монитора (если не **`-SkipStartMonitorAfterUpdate`**). +4. Версия на шаре **новее** (или сработало условие выше) — остановка процессов → копирование **`Login_Monitor.ps1`** и **`Sac-Client.ps1`** → настройка SAC в settings при необходимости → **`Login_Monitor.ps1 -InstallTasks`** → **`deployed_version.txt`** → запуск монитора. 5. Версия на шаре **старее** — откат блокируется, пока не указан **`-AllowDowngrade`**. @@ -44,8 +46,9 @@ - Секреты и параметры сайта (Telegram, SMTP, **4740**, `$IgnoreAdvapiNetworkLogonSourceIps`) — **только** в этом файле. - Образец в репозитории и на шаре: **`login_monitor.settings.example.ps1`**. -- **`Deploy-LoginMonitor.ps1`** **не перезаписывает** `login_monitor.settings.ps1` при обновлении скрипта. -- Если файла нет, Deploy **один раз** копирует example → settings (дальше правки только локально). +- **`Deploy-LoginMonitor.ps1`** **не перезаписывает** существующий `login_monitor.settings.ps1`, если SAC уже настроен (`UseSAC` не `off`, заданы `$SacUrl` и `$SacApiKey`). +- Если файла нет — Deploy **один раз** копирует example → settings (**`UseSAC = 'dual'`**). +- Если файл есть, но **нет блока SAC** — Deploy **дописывает** блок из example (Telegram/SMTP не трогает). ```powershell $root = 'C:\ProgramData\RDP-login-monitor' diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1 index bcd9688..c7a7eb4 100644 --- a/Login_Monitor.ps1 +++ b/Login_Monitor.ps1 @@ -80,7 +80,7 @@ $script:MonitorLoopInitialized = $false # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). -$ScriptVersion = "1.2.18-SAC" +$ScriptVersion = "1.2.20-SAC" # Логи (все под InstallRoot) $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" diff --git a/tools/Test-DeploySyntax.ps1 b/tools/Test-DeploySyntax.ps1 new file mode 100644 index 0000000..66e2cce --- /dev/null +++ b/tools/Test-DeploySyntax.ps1 @@ -0,0 +1,8 @@ +$errs = $null +[void][System.Management.Automation.Language.Parser]::ParseFile( + (Join-Path $PSScriptRoot '..\Deploy-LoginMonitor.ps1'), + [ref]$null, + [ref]$errs +) +if ($errs) { $errs | ForEach-Object { $_.ToString() }; exit 1 } +Write-Output 'OK' diff --git a/version.txt b/version.txt index 47ec3d6..c789d67 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.2.18-SAC +1.2.20-SAC