diff --git a/.gitignore b/.gitignore index dd5f811..d4ea9b1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ -.cursor/ +.cursor/ tools/*.log *.log +*.bak +Logs/ +sac-spool/ +login_monitor.settings.ps1 +exchange_monitor.settings.ps1 diff --git a/Docs/exchange-mail-security.md b/Docs/exchange-mail-security.md index 065733c..7664458 100644 --- a/Docs/exchange-mail-security.md +++ b/Docs/exchange-mail-security.md @@ -1,4 +1,4 @@ -# Exchange Mail Security — руководство +# Exchange Mail Security — руководство Скрипт **`Exchange-MailSecurity.ps1`** предназначен **только для сервера Microsoft Exchange** с Exchange Management Shell (EMS). Не устанавливается на все компьютеры домена через GPO RDP-монитора. @@ -68,6 +68,16 @@ - Каналы: **Telegram** и/или **Email** (модуль **`Notify-Common.ps1`**). - Пересылка: **`$AlertOnlyOnNewForwardingFindings = $true`** — алерт при **новой** находке (`Logs\exchange_forwarding_baseline.json`). - **Первый скан:** **`$SuppressAlertsOnFirstBaselineRun = $true`** (по умолчанию) — существующие пересылки **только в baseline**, без всплеска алертов; одна **сводка** (`$SendInboxScanSummary`). + +### Dry-run перед первым Inbox-сканом + +```powershell +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "\\dc.contoso.local\NETLOGON\RDP-login-monitor\Exchange-MailSecurity.ps1" -Mode Inbox -WhatIf +``` + +`-WhatIf` подключает EMS, считает объём (`Get-Mailbox` / VIP-фильтр), **не вызывает** `Get-InboxRule`, не шлёт уведомления и не пишет baseline. Рекомендуется перед `-InstallTasks` и первым ночным `-Mode Inbox`. + +При полном скане без VIP скрипт пишет **WARN** в лог; проблемные ящики — в **`$SkipInboxScanMailboxes`**. - Далее — алерт только при **новых** или **изменённых** пересылках (в т.ч. включили ранее отключённое правило). - **`$NotifyWhenForwardingScanClean = $false`** — не слать «всё чисто» при нуле находок. diff --git a/Exchange-MailSecurity.ps1 b/Exchange-MailSecurity.ps1 index 4065e8d..e984d6b 100644 --- a/Exchange-MailSecurity.ps1 +++ b/Exchange-MailSecurity.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Мониторинг Exchange: очереди транспорта, пересылка на внешние адреса (Inbox rules + mailbox forwarding + transport rules). .DESCRIPTION @@ -10,12 +10,13 @@ Опционально: exchange_monitor.settings.ps1 в том же каталоге (секреты, whitelist). #> -[CmdletBinding()] +[CmdletBinding(SupportsShouldProcess = $true)] param( [ValidateSet('Queues', 'Inbox', 'Watchdog')] [string]$Mode = 'Queues', [switch]$InstallTasks, - [switch]$Watchdog + [switch]$Watchdog, + [switch]$WhatIf ) Set-StrictMode -Version Latest @@ -25,7 +26,7 @@ $ErrorActionPreference = 'Stop' # КОНФИГУРАЦИЯ # ============================================ -$ScriptVersion = '1.6.7' +$ScriptVersion = '1.6.8' $script:InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor") $script:CanonicalScriptName = 'Exchange-MailSecurity.ps1' $LogFile = Join-Path $script:InstallRoot 'Logs\exchange_mail_security.log' @@ -100,6 +101,18 @@ if (Test-Path -LiteralPath $SettingsFile) { . $SettingsFile } +function Write-ExchangeScanSafetyWarnings { + if (-not $SuppressAlertsOnFirstBaselineRun) { + Write-ExchLog 'WARN: SuppressAlertsOnFirstBaselineRun=$false — первый Inbox-скан может разослать алерты по всем уже существующим пересылкам.' + } + if (-not $VipMailboxesOnly -and $MaxMailboxesPerRun -le 0 -and $ScanInboxRules) { + Write-ExchLog 'WARN: полный скан Inbox rules по всем ящикам (VipMailboxesOnly=$false). Рекомендуется пилот: VipMailboxesOnly=$true или -Mode Inbox -WhatIf.' + } + if (@($SkipInboxScanMailboxes | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }).Count -eq 0) { + Write-ExchLog 'TIP: добавьте проблемные ящики в $SkipInboxScanMailboxes, если Get-InboxRule падает (corrupt rule store).' + } +} + function Write-NotifyLog { param([string]$Message) Write-ExchLog $Message @@ -235,6 +248,7 @@ if ($InstallTasks) { Send-ExchangeInstallNotification Write-ExchLog 'InstallTasks: install notification sent' } + Write-ExchLog 'InstallTasks: перед первым ночным Inbox-сканом выполните: Exchange-MailSecurity.ps1 -Mode Inbox -WhatIf' exit 0 } @@ -518,6 +532,15 @@ function Invoke-ExchangeQueueScan { Write-ExchLog "Queues: threshold MessageCount > $QueueMessageCountThreshold" $queues = @(Get-Queue -ErrorAction Stop) + if ($WhatIf) { + $hot = @($queues | Where-Object { $_.MessageCount -gt $QueueMessageCountThreshold }) + Write-ExchLog "WhatIf: queues total=$($queues.Count), above threshold=$($hot.Count) — alerts skipped" + foreach ($q in $hot) { + Write-ExchLog "WhatIf: would alert queue=$($q.Identity) messages=$($q.MessageCount)" + } + return + } + $hot = @($queues | Where-Object { $_.MessageCount -gt $QueueMessageCountThreshold }) $state = Get-QueueAlertState $now = Get-Date @@ -760,6 +783,20 @@ function Send-ExchangeInboxScanSummary { function Invoke-ExchangeInboxAndForwardingScan { $scopeLabel = Get-ExchangeInboxScanScopeLabel Write-ExchLog "Inbox/Forwarding scan v$ScriptVersion; scope: $scopeLabel; notify: $(Get-NotifyChainHuman)" + if ($WhatIf) { + $null = Import-ExchangeManagementShell + $internalDomains = Get-InternalAcceptedDomainNames + Write-ExchLog "WhatIf: accepted domains: $(@($internalDomains) -join ', ')" + $mailboxCount = 0 + if ($ScanInboxRules) { + $mailboxes = @(Get-MailboxListForScan) + $mailboxCount = $mailboxes.Count + Write-ExchLog "WhatIf: would scan $mailboxCount mailboxes ($scopeLabel); Get-InboxRule not called" + } + Write-ExchLog "WhatIf: ScanMailboxForwarding=$ScanMailboxForwarding ScanTransportRules=$ScanTransportRules — no alerts, no baseline write" + return + } + $null = Import-ExchangeManagementShell $internalDomains = Get-InternalAcceptedDomainNames Write-ExchLog "Accepted domains (internal): $(@($internalDomains) -join ', ')" @@ -879,7 +916,8 @@ if (-not (Test-RunningElevated)) { Write-ExchLog 'WARNING: not running elevated - EMS/tasks may fail.' } -Write-ExchLog "=== Exchange-MailSecurity v$ScriptVersion Mode=$Mode ===" +Write-ExchLog "=== Exchange-MailSecurity v$ScriptVersion Mode=$Mode$(if ($WhatIf) { ' WhatIf' }) ===" +Write-ExchangeScanSafetyWarnings try { switch ($Mode) { diff --git a/Install-ScheduledTasks.ps1 b/Install-ScheduledTasks.ps1 deleted file mode 100644 index b0ea9b6..0000000 --- a/Install-ScheduledTasks.ps1 +++ /dev/null @@ -1,87 +0,0 @@ -#Requires -RunAsAdministrator -<# -.SYNOPSIS - Регистрирует в Планировщике заданий основной монитор и watchdog (как в README). -.DESCRIPTION - Запускайте из повышенной PowerShell. Пути по умолчанию — D:\Soft\. - Watchdog использует Watchdog_RDP_Monitor.ps1 из репозитория (проверка процесса и heartbeat). -#> -[CmdletBinding()] -param( - [string]$InstallRoot = "D:\Soft", - [string]$MainTaskName = "RDP Login Monitor", - [string]$WatchdogTaskName = "RDP Login Monitor Watchdog", - [int]$WatchdogRepeatMinutes = 5, - [int]$MainStartupRandomDelayMinutes = 1, - [int]$WatchdogStartupRandomDelayMinutes = 2 -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$LoginScriptPath = Join-Path $InstallRoot "Login_Monitor.ps1" -$WatchdogScriptPath = Join-Path $InstallRoot "Watchdog_RDP_Monitor.ps1" -$LogsDir = Join-Path $InstallRoot "Logs" - -if (-not (Test-Path -LiteralPath $LoginScriptPath)) { - throw "Не найден основной скрипт: $LoginScriptPath" -} -if (-not (Test-Path -LiteralPath $WatchdogScriptPath)) { - throw "Не найден watchdog: $WatchdogScriptPath" -} -if (-not (Test-Path -LiteralPath $LogsDir)) { - New-Item -ItemType Directory -Path $LogsDir -Force | Out-Null -} - -$principal = New-ScheduledTaskPrincipal ` - -UserId "NT AUTHORITY\SYSTEM" ` - -LogonType ServiceAccount ` - -RunLevel Highest - -# --- Задание 1: основной монитор --- -$mainArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$LoginScriptPath`"" -$mainAction = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument $mainArgs -$mainTrigger = New-ScheduledTaskTrigger -AtStartup -RandomDelay (New-TimeSpan -Minutes $MainStartupRandomDelayMinutes) -$mainSettings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -MultipleInstances IgnoreNew ` - -ExecutionTimeLimit ([TimeSpan]::Zero) - -Register-ScheduledTask ` - -TaskName $MainTaskName ` - -Action $mainAction ` - -Trigger $mainTrigger ` - -Principal $principal ` - -Settings $mainSettings ` - -Force | Out-Null - -Write-Host "Создано задание: $MainTaskName" -ForegroundColor Cyan - -# --- Задание 2: watchdog (старт + периодический запуск, как в README) --- -$wdArgs = "-NoProfile -ExecutionPolicy Bypass -WindowStyle Hidden -File `"$WatchdogScriptPath`"" -$wdAction = New-ScheduledTaskAction -Execute "PowerShell.exe" -Argument $wdArgs -$wdTriggerStartup = New-ScheduledTaskTrigger -AtStartup -RandomDelay (New-TimeSpan -Minutes $WatchdogStartupRandomDelayMinutes) -$repeatDuration = New-TimeSpan -Days 3650 -$anchor = (Get-Date).AddMinutes([Math]::Max(3, $WatchdogRepeatMinutes)) -$wdTriggerRepeat = New-ScheduledTaskTrigger -Once -At $anchor ` - -RepetitionInterval (New-TimeSpan -Minutes $WatchdogRepeatMinutes) ` - -RepetitionDuration $repeatDuration - -$wdSettings = New-ScheduledTaskSettingsSet ` - -AllowStartIfOnBatteries ` - -DontStopIfGoingOnBatteries ` - -StartWhenAvailable ` - -MultipleInstances IgnoreNew - -Register-ScheduledTask ` - -TaskName $WatchdogTaskName ` - -Action $wdAction ` - -Trigger @($wdTriggerStartup, $wdTriggerRepeat) ` - -Principal $principal ` - -Settings $wdSettings ` - -Force | Out-Null - -Write-Host "Создано задание: $WatchdogTaskName (триггеры: при старте ОС и каждые $WatchdogRepeatMinutes мин.)" -ForegroundColor Cyan -Write-Host "Готово. При необходимости сразу запустите: Start-ScheduledTask -TaskName '$MainTaskName'" -ForegroundColor Green diff --git a/Login_Monitor.ps1 b/Login_Monitor.ps1 index bc55350..5dab13b 100644 --- a/Login_Monitor.ps1 +++ b/Login_Monitor.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Мониторинг логинов/попыток входа с уведомлениями в Telegram .DESCRIPTION @@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15 # строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах). # Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только # исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1). -$ScriptVersion = "2.1.9-SAC" +$ScriptVersion = "2.1.10-SAC" # Логи (все под InstallRoot) $LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log" @@ -1140,6 +1140,9 @@ if (-not (Test-Administrator)) { Write-Log "Скрипт запущен с правами администратора, версия $ScriptVersion" if ($script:LoginMonitorSettingsLoaded) { Write-Log "Настройки: login_monitor.settings.ps1 загружен." + if ($SacTlsSkipVerify) { + Write-Log 'CRITICAL: SacTlsSkipVerify=$true — проверка TLS для SAC отключена (риск MITM). Только для краткого отладочного окна в lab.' + } } else { Write-Log "Предупреждение: login_monitor.settings.ps1 не найден — Telegram/SMTP/4740 не настроены (скопируйте login_monitor.settings.example.ps1)." } diff --git a/README.md b/README.md index a56881d..0d8cdc3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# RDP Login Monitor +# RDP Login Monitor -**Версия:** `2.1.5-SAC` (`$ScriptVersion` + `version.txt`) +**Версия:** `2.1.10-SAC` (`$ScriptVersion` + `version.txt`) PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты. diff --git a/README_eng.md b/README_eng.md index 93f2465..9626956 100644 --- a/README_eng.md +++ b/README_eng.md @@ -92,7 +92,7 @@ For domain deployment from a share you do not configure the scheduler on clients - **`-SkipScheduledTaskMaintenance`**: during normal monitor startup, skip verification/recreation of scheduled tasks (if you manage tasks only via **`-InstallTasks`** or manually). - **`Install-DeployScheduledTask.ps1`** — helper to run **`Deploy-LoginMonitor.ps1`** from a share on a schedule (see **[DEPLOY.md](DEPLOY.md)**). -- **`Watchdog_RDP_Monitor.ps1`** and **`Install-ScheduledTasks.ps1`** — **alternate** layout with a separate watchdog script and default paths under **`D:\Soft`**. For new installs, prefer the built-in **`-Watchdog`** in **`Login_Monitor.ps1`** and tasks **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`**. +- Watchdog is built into **`Login_Monitor.ps1`** (`-Watchdog`); scheduled tasks **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`** are registered by **`-InstallTasks`**. - **`ignore.lst.example`** in the repo is a template for **`ignore.lst`** to suppress selected Security notifications (see section 7). - **`login_monitor.settings.example.ps1`** — template for **`login_monitor.settings.ps1`** (Telegram, SMTP, 4740, local IP exclusions). Deploy may create `login_monitor.settings.ps1` from the example on first install. diff --git a/Sac-Client.ps1 b/Sac-Client.ps1 index 54df8e1..c3959c1 100644 --- a/Sac-Client.ps1 +++ b/Sac-Client.ps1 @@ -1,4 +1,4 @@ -<# +<# .SYNOPSIS Клиент Security Alert Center для RDP-login-monitor. .DESCRIPTION @@ -420,6 +420,7 @@ function Test-SacShouldAttemptSend { function Invoke-SacTlsPrep { if (-not $SacTlsSkipVerify) { return } if (-not $script:SacTlsCallbackRegistered) { + Write-SacLog 'CRITICAL: SacTlsSkipVerify=$true — TLS certificate validation disabled for SAC (MITM risk). Use only for short-lived lab debugging.' [System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true } $script:SacTlsCallbackRegistered = $true } diff --git a/Watchdog_RDP_Monitor.ps1 b/Watchdog_RDP_Monitor.ps1 deleted file mode 100644 index a472120..0000000 --- a/Watchdog_RDP_Monitor.ps1 +++ /dev/null @@ -1,117 +0,0 @@ -<# -.SYNOPSIS - Watchdog для Login_Monitor.ps1 -.DESCRIPTION - Проверяет, запущен ли основной скрипт Login_Monitor.ps1. - Если нет — запускает его и пишет лог. - Дополнительно проверяет heartbeat-файл и перезапускает скрипт, если heartbeat "протух". -#> - -[CmdletBinding()] -param( - [string]$MainScriptPath = "D:\Soft\Login_Monitor.ps1", - [string]$HeartbeatFile = "D:\Soft\Logs\last_heartbeat.txt", - [int]$HeartbeatStaleMinutes = 90, - [string]$WatchdogLog = "D:\Soft\Logs\watchdog.log" -) - -Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" - -$script:Utf8BomEncoding = New-Object System.Text.UTF8Encoding $true -$script:WatchdogLogBomChecked = $false - -function Ensure-FileStartsWithUtf8Bom { - param([Parameter(Mandatory = $true)][string]$Path) - if (-not (Test-Path -LiteralPath $Path)) { return } - $bytes = [System.IO.File]::ReadAllBytes($Path) - if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) { return } - $bom = [byte[]](0xEF, 0xBB, 0xBF) - $combined = New-Object byte[] ($bom.Length + $bytes.Length) - [Buffer]::BlockCopy($bom, 0, $combined, 0, $bom.Length) - if ($bytes.Length -gt 0) { - [Buffer]::BlockCopy($bytes, 0, $combined, $bom.Length, $bytes.Length) - } - [System.IO.File]::WriteAllBytes($Path, $combined) -} - -function Write-WatchdogLog { - param([string]$Message) - $ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss" - $line = "$ts - $Message" + [Environment]::NewLine - $dir = Split-Path -Parent $WatchdogLog - if ($dir -and -not (Test-Path $dir)) { - New-Item -ItemType Directory -Path $dir -Force | Out-Null - } - if (-not $script:WatchdogLogBomChecked) { - Ensure-FileStartsWithUtf8Bom -Path $WatchdogLog - $script:WatchdogLogBomChecked = $true - } - [System.IO.File]::AppendAllText($WatchdogLog, $line, $script:Utf8BomEncoding) -} - -function Get-MainScriptProcesses { - try { - $procs = Get-CimInstance Win32_Process -Filter "Name = 'powershell.exe' OR Name = 'pwsh.exe'" -ErrorAction Stop - return $procs | Where-Object { $_.CommandLine -and ($_.CommandLine -like "*$MainScriptPath*") } - } catch { - Write-WatchdogLog "Ошибка проверки процессов: $($_.Exception.Message)" - return @() - } -} - -function Start-MainScript { - if (-not (Test-Path $MainScriptPath)) { - Write-WatchdogLog "Основной скрипт не найден: $MainScriptPath" - return - } - $args = "-NoProfile -ExecutionPolicy Bypass -File `"$MainScriptPath`"" - Start-Process -FilePath "powershell.exe" -ArgumentList $args -WindowStyle Hidden | Out-Null - Write-WatchdogLog "Основной скрипт запущен: $MainScriptPath" -} - -function Stop-MainScript { - $procs = Get-MainScriptProcesses - foreach ($p in $procs) { - try { - Stop-Process -Id $p.ProcessId -Force -ErrorAction Stop - Write-WatchdogLog "Остановлен зависший экземпляр PID=$($p.ProcessId)" - } catch { - Write-WatchdogLog "Ошибка остановки PID=$($p.ProcessId): $($_.Exception.Message)" - } - } -} - -function Is-HeartbeatStale { - if (-not (Test-Path $HeartbeatFile)) { - Write-WatchdogLog "Heartbeat файл отсутствует: $HeartbeatFile" - return $true - } - try { - $raw = (Get-Content $HeartbeatFile -ErrorAction Stop | Select-Object -First 1).Trim() - if (-not $raw) { return $true } - $hb = [datetime]::ParseExact($raw, "dd.MM.yyyy HH:mm:ss", $null) - $age = (Get-Date) - $hb - return ($age.TotalMinutes -gt $HeartbeatStaleMinutes) - } catch { - Write-WatchdogLog "Ошибка чтения heartbeat: $($_.Exception.Message)" - return $true - } -} - -$running = Get-MainScriptProcesses -if (-not $running -or $running.Count -eq 0) { - Write-WatchdogLog "Основной скрипт не запущен, выполняю старт." - Start-MainScript - exit 0 -} - -if (Is-HeartbeatStale) { - Write-WatchdogLog "Heartbeat устарел, перезапускаю основной скрипт." - Stop-MainScript - Start-Sleep -Seconds 2 - Start-MainScript -} else { - Write-WatchdogLog "Проверка пройдена: процесс запущен, heartbeat свежий." -} - diff --git a/scripts/Push-GitHubMirror.ps1 b/scripts/Push-GitHubMirror.ps1 new file mode 100644 index 0000000..df55040 --- /dev/null +++ b/scripts/Push-GitHubMirror.ps1 @@ -0,0 +1,39 @@ +# Push sanitized main to GitHub without leaving secrets on local main (kalinamall workflow). +# Usage: .\scripts\Push-GitHubMirror.ps1 +param( + [string]$Remote = 'github', + [string]$Branch = 'main' +) + +$ErrorActionPreference = 'Stop' +$Root = Split-Path -Parent $PSScriptRoot +Set-Location $Root + +git remote get-url $Remote 2>$null | Out-Null +if ($LASTEXITCODE -ne 0) { + throw "remote not configured: $Remote" +} + +if ((git status --porcelain)) { + throw 'working tree not clean; commit or stash first' +} + +$before = (git rev-parse HEAD).Trim() +Write-Output "local HEAD before GitHub push: $before" + +& "$PSScriptRoot\Sanitize-ForGitHub.ps1" +& "$PSScriptRoot\Rewrite-GitHostUrls.ps1" -Target github +& "$PSScriptRoot\Test-NoSecretsForGitHub.ps1" + +$status = git status --porcelain +if (-not $status) { + Write-Output 'no changes after sanitize; pushing current HEAD to GitHub' + git push $Remote $Branch + exit 0 +} + +git add -A +git commit -m "chore(github): sanitize secrets and sync public mirror URLs" +git push --force-with-lease $Remote $Branch +git reset --hard $before +Write-Output "pushed $Remote/$Branch (force-with-lease mirror); local main restored to $before (production/kalinamall)" diff --git a/scripts/Push-Mirror.ps1 b/scripts/Push-Mirror.ps1 index 4eda468..a97c499 100644 --- a/scripts/Push-Mirror.ps1 +++ b/scripts/Push-Mirror.ps1 @@ -1,4 +1,4 @@ -# Push main to a mirror remote with host-specific doc URLs, without leaving URL churn on main. +# Push main to a mirror remote with host-specific doc URLs, without leaving URL churn on main. # Usage: .\scripts\Push-Mirror.ps1 github|kalinamall|papatramp param( [Parameter(Mandatory = $true)] @@ -11,7 +11,7 @@ $Root = Split-Path -Parent $PSScriptRoot Set-Location $Root $remote = switch ($Target) { - 'github' { 'origin' } + 'github' { 'github' } 'kalinamall' { 'kalinamall' } 'papatramp' { 'papatramp' } } diff --git a/scripts/Sanitize-ForGitHub.ps1 b/scripts/Sanitize-ForGitHub.ps1 new file mode 100644 index 0000000..c5a3f30 --- /dev/null +++ b/scripts/Sanitize-ForGitHub.ps1 @@ -0,0 +1,155 @@ +# Replace production-only values with public-safe placeholders (GitHub mirror). +# Usage: .\scripts\Sanitize-ForGitHub.ps1 +# Reversible: production copies live on kalinamall/papatramp; restore via git reset --hard. +param( + [switch]$WhatIf +) + +$ErrorActionPreference = 'Stop' +$Root = Split-Path -Parent $PSScriptRoot +Set-Location $Root + +function Set-TrackedFileText { + param( + [Parameter(Mandatory = $true)][string]$RelativePath, + [Parameter(Mandatory = $true)][string]$Content + ) + $path = Join-Path $Root $RelativePath + if ($WhatIf) { + Write-Output "WhatIf: would write $RelativePath" + return + } + $utf8Bom = New-Object System.Text.UTF8Encoding $true + [System.IO.File]::WriteAllText($path, $Content.TrimEnd() + "`r`n", $utf8Bom) + Write-Output "sanitized: $RelativePath" +} + +$loginSettings = @' +<# +.SYNOPSIS + Локальные настройки Login_Monitor.ps1 +.DESCRIPTION + Скопируйте в C:\ProgramData\RDP-login-monitor\login_monitor.settings.ps1 + и при необходимости отредактируйте. Deploy-LoginMonitor.ps1 не перезаписывает settings, + если SAC уже настроен (UseSAC не off и задан SacApiKey). При первой установке или апгрейде + с версии без SAC (нет Sac-Client.ps1 / пустой ключ) example копируется поверх с резервной .bak. +#> + +# --- Telegram (или DPAPI Base64 через Encrypt-DpapiForRdpMonitor.ps1) --- +$TelegramBotToken = 'YOUR_BOT_TOKEN' +$TelegramChatID = 'YOUR_CHAT_ID' +# $TelegramBotTokenProtectedB64 = '' +# $TelegramChatIDProtectedB64 = '' + +# --- Email (опционально) --- +$NotifyOrder = 'tg' +# $MailSmtpHost = 'smtp.example.com' +# $MailSmtpPort = 587 +# $MailSmtpUser = '' +# $MailSmtpPassword = '' +# $MailFrom = 'monitor@example.com' +# $MailTo = 'admin@example.com' +# $MailSmtpStartTls = $true +# $MailSmtpSsl = $false +# $MailSmtpPasswordProtectedB64 = '' + +# --- Подпись сервера в Telegram и SAC (host.display_name); пусто = $env:COMPUTERNAME --- +# $ServerDisplayName = 'RDP-Server-01' +# --- Явный IPv4 хоста для SAC (опционально; иначе автоопределение) --- +# $ServerIPv4 = '192.168.1.10' + +# --- Security Alert Center (SAC) --- +# off | exclusive | dual | fallback — см. security-alert-center/docs/agent-integration.md +$UseSAC = 'fallback' +$SacUrl = 'https://sac.example.com' +$SacApiKey = 'sac_CHANGE_ME' +$SacSpoolDir = 'C:\ProgramData\RDP-login-monitor\sac-spool' +$SacTimeoutSec = 45 +$SacSpoolFlushMaxFiles = 50 +$SacSpoolMaxAgeHours = 72 +# $SacTlsSkipVerify = $false +# $SacFallbackFailures = 5 +# $false = не слать report.daily.rdp с агента (суточный отчёт только из SAC) +# В settings.ps1 используйте 1/0 или $true/$false — не пишите голое false без $ +$DailyReportEnabled = 1 + +# --- Heartbeat SAC (agent.heartbeat): интервал в секундах; 14400 = 4 ч --- +$HeartbeatInterval = 14400 +# Оповещение, если last_heartbeat.txt не обновлялся > множитель × интервал (2 × 4 ч = 8 ч) +$HeartbeatStaleAlertMultiplier = 2 +# Poll SAC на команды qwinsta/logoff (сек); см. security-alert-center/docs/agent-control-plane.md +# $SacCommandPollIntervalSec = 60 + +# Окно (мин): LastBootUpTime + System 41/1074/6005/6008/6009 → «старт после перезагрузки ОС» +$StartupRebootDetectMinutes = 5 + +# --- Инвентаризация железа/ПО для SAC (agent.inventory, раз в 12 ч) --- +$GetInventory = $true + +# --- RDS Shadow Control + WinRM inbound (Enter-PSSession), severity warning --- +# $EnableRcm1149Monitoring = 1 # RCM Operational 1149 (RDP auth; workstation + RDS server) +# $EnableRcmShadowControlMonitoring = 1 # RCM Operational 20506/20507/20510 +# $EnableWinRmInboundMonitoring = 1 # WinRM Operational 91 (+ correlate Security 4624) +# $EnableAdminShareMonitoring = 1 # Security 5140 C$/ADMIN$ (audit File Share) +# $WinRmIgnoreLocalSource = 1 # ::1, 127.0.0.1, fe80 (шум Exchange/локальный WinRM) +# $WinRmIgnoreMachineAccounts = 1 # учётки, оканчивающиеся на $ +# $WinRmExchangeStrictMode = 1 # Exchange: user в Event 91 обязателен; 4624 только LogonProcess WinRM +# HealthMailbox* уже в ExcludedUserPatterns скрипта +# Проверка: powershell -File Login_Monitor.ps1 -CheckSac + +# --- Узкое исключение шумовых сетевых логонов (LogonType=3, Advapi) --- +$IgnoreAdvapiNetworkLogonSourceIps = @( + '192.168.1.10' +) +# --- Exchange noise filter: 4624 + LogonType=3 + IP='-' (часто Outlook/почтовые клиенты) --- +# Включайте на почтовом сервере, если нужен только полезный интерактивный сигнал. +${Ignore4624-LT3-EmptyIP-Event} = $false + +# --- Ротация login_monitor.log и хранение бэкапов (Logs\Backup\LoginLog_*.bak) --- +# $LogRotationHour = 0 +# $LogRotationMinute = 0 +$MaxBackupDays = 31 + +# --- Блокировка учётной записи AD (4740) + IP из IIS ActiveSync --- +# Мониторинг включается только на КД с именем $LockoutMonitorDomainController. +$LockoutMonitorDomainController = 'dc01.contoso.local' +$NetBiosDomainName = 'CONTOSO' +$ExchangeIisLogPath = '\\mail.contoso.local\c$\inetpub\logs\LogFiles\W3SVC1' +$ExchangeServerHostForIisExclude = '' +$ExchangeIisLogTailLines = 5000 +$ExchangeIisLogMinutesBeforeLockout = 30 +'@ + +Set-TrackedFileText -RelativePath 'login_monitor.settings.example.ps1' -Content $loginSettings + +$exchangeSettingsPath = Join-Path $Root 'exchange_monitor.settings.example.ps1' +if (Test-Path -LiteralPath $exchangeSettingsPath) { + $ex = Get-Content -LiteralPath $exchangeSettingsPath -Raw + $ex = $ex -replace 'kalinamall\.ru', 'example.com' + $ex = $ex -replace 'fifth\.example\.com', 'mail.contoso.local' + $ex = $ex -replace 'k\.selezneva@example\.com', 'broken-mailbox@example.com' + Set-TrackedFileText -RelativePath 'exchange_monitor.settings.example.ps1' -Content $ex +} + +$updatePath = Join-Path $Root 'update-rdp-monitor.ps1' +if (Test-Path -LiteralPath $updatePath) { + $upd = Get-Content -LiteralPath $updatePath -Raw + $upd = $upd -replace "Posle fetch: vsegda reset --hard na kalinamall/main \(bez merge\), zatem clean -fd\.", + 'Posle fetch: reset --hard na upstream/main (bez merge), zatem clean -fd.' + $upd = $upd -replace "\\\\b26\\NETLOGON\\RDP-login-monitor", '\\dc.contoso.local\NETLOGON\RDP-login-monitor' + $upd = $upd -replace "https://git\.kalinamall\.ru/PapaTramp/RDP-login-monitor\.git", + 'https://github.com/PTah/RDP-login-monitor.git' + Set-TrackedFileText -RelativePath 'update-rdp-monitor.ps1' -Content $upd +} + +$netlogonDoc = Join-Path $Root 'Docs/deploy-netlogon-publish.md' +if (Test-Path -LiteralPath $netlogonDoc) { + $doc = Get-Content -LiteralPath $netlogonDoc -Raw + $doc = $doc -replace 'K6A-DC3', 'dc01.corp.example.com' + $doc = $doc -replace '\\\\b26\\', '\\dc.contoso.local\' + $doc = $doc -replace "https://git\.kalinamall\.ru/PapaTramp/RDP-login-monitor\.git", + 'https://git.example.com/org/RDP-login-monitor.git' + Set-TrackedFileText -RelativePath 'Docs/deploy-netlogon-publish.md' -Content $doc +} + +Write-Output 'Sanitize-ForGitHub: done' diff --git a/scripts/Test-NoSecretsForGitHub.ps1 b/scripts/Test-NoSecretsForGitHub.ps1 new file mode 100644 index 0000000..badceb5 --- /dev/null +++ b/scripts/Test-NoSecretsForGitHub.ps1 @@ -0,0 +1,46 @@ +# Fail if tracked text still contains production-only markers (run before GitHub push). +$ErrorActionPreference = 'Stop' +$Root = Split-Path -Parent $PSScriptRoot +Set-Location $Root + +$patterns = @( + '8239219522', + 'sac_UkOsAT', + '2843230', + 'sac\.kalinamall\.ru', + '\\\\b26\\', + 'K6A-DC3', + 'fifth\.kalinamall', + '192\.168\.160\.57', + 'kalinamall\.ru', + 'git\.kalinamall\.ru', + 'git\.papatramp\.ru', + '\d{8,12}:[A-Za-z0-9_-]{20,}' +) + +$extensions = @('*.md', '*.ps1', '*.example', '*.txt', '*.json', '*.yml', '*.yaml') +$files = git ls-files $extensions 2>$null | Where-Object { $_ -and (Test-Path $_) } + +$hits = @() +foreach ($file in $files) { + if ($file -like 'scripts/Rewrite-GitHostUrls.ps1') { continue } + if ($file -like 'scripts/Push-PrivateMirror.ps1') { continue } + if ($file -like 'scripts/Sanitize-ForGitHub.ps1') { continue } + if ($file -like 'scripts/Test-NoSecretsForGitHub.ps1') { continue } + if ($file -like 'scripts/Push-GitHubMirror.ps1') { continue } + + $text = Get-Content -LiteralPath $file -Raw -ErrorAction SilentlyContinue + if ([string]::IsNullOrEmpty($text)) { continue } + + foreach ($pat in $patterns) { + if ($text -match $pat) { + $hits += "${file}: matches /$pat/" + } + } +} + +if ($hits.Count -gt 0) { + Write-Error ("GitHub secret scan failed:`n" + ($hits -join "`n")) +} + +Write-Output "GitHub secret scan: OK ($($files.Count) files)" diff --git a/version.txt b/version.txt index 4f49bf9..45a6028 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -2.1.9-SAC +2.1.10-SAC