Compare commits
180 Commits
main
..
599b152296
| Author | SHA1 | Date | |
|---|---|---|---|
| 599b152296 | |||
| c8dc681289 | |||
| c4c0556a2d | |||
| 2f44ddc29d | |||
| 6d160cef16 | |||
| 2e262ae878 | |||
| 6f08af7365 | |||
| afa80d169f | |||
| 7f38c00094 | |||
| c12f169189 | |||
| 37fdeb44c5 | |||
| 746f9e23b2 | |||
| 35e25063e3 | |||
| 7466254401 | |||
| 8d17a0f560 | |||
| ae86cb3ea7 | |||
| c146bb119a | |||
| 1fcd5cb5cf | |||
| f48aee1d93 | |||
| d5db38e2ae | |||
| 91cde1668c | |||
| 09f1b576bf | |||
| b6d293d21b | |||
| 6ee5fdcbf2 | |||
| 0da1d0f2ac | |||
| 5e9939724c | |||
| 31ebd5d653 | |||
| 5c23ac5f36 | |||
| 6fa940dd43 | |||
| 463a3ef08c | |||
| 399597c8a1 | |||
| 365a7113cf | |||
| 01d7525c4a | |||
| 1d30ca2284 | |||
| 80b2337d61 | |||
| fe2e6d624f | |||
| 82c63fb9a8 | |||
| bd83dc84dc | |||
| afded8a495 | |||
| 5fc5f2a9ad | |||
| e3149785dc | |||
| 6384a45395 | |||
| 0acd591c83 | |||
| fa5248d501 | |||
| f08a8a8561 | |||
| 550cea9759 | |||
| e00513f050 | |||
| 22ac7a32c7 | |||
| 92773948a7 | |||
| 7b83a19db0 | |||
| 33367b05a8 | |||
| 94d12e0b5f | |||
| 4b8ff95344 | |||
| 47f95ba186 | |||
| 335768915b | |||
| 5e5bc86e1b | |||
| 42d6af83f2 | |||
| 52e9e62ede | |||
| fae185421a | |||
| afc7bf9dca | |||
| ac095fe1a0 | |||
| 81323e8596 | |||
| 1eedfc736f | |||
| d54d04623f | |||
| 084494cfa8 | |||
| 1a1467f910 | |||
| 543369832a | |||
| f970e284ac | |||
| 6a9b2b70dc | |||
| 2118d4a4a0 | |||
| 78f241df32 | |||
| e1ae9834a9 | |||
| a74c8df877 | |||
| 2395f536cd | |||
| b56ad9c7e4 | |||
| e738d97c9c | |||
| 1fe8bd9f7d | |||
| bf04c8865c | |||
| b010ad01eb | |||
| 1ff8dd1a95 | |||
| 724baa6c0a | |||
| 2bbf1face6 | |||
| d23e99a885 | |||
| 70f06790f6 | |||
| c249451eba | |||
| 1f27272883 | |||
| 49359c07a6 | |||
| 611d9c68ca | |||
| 737da07d6a | |||
| b9d385f51b | |||
| e7f19e5c92 | |||
| e4f4d1bb1f | |||
| 74d1e4db75 | |||
| 5284734092 | |||
| 1c210a40d8 | |||
| 83315cc3b8 | |||
| 04f69aac40 | |||
| 6c86e172b9 | |||
| 71cb1fa9b6 | |||
| ef237f345b | |||
| 1092f6340d | |||
| 9030fb3c30 | |||
| 3ed20b130f | |||
| 1b4ca5fa01 | |||
| a1a3bca0c3 | |||
| 004a8c0026 | |||
| 904ab13880 | |||
| 22a32bc1cb | |||
| bd8076a1d0 | |||
| edbe00ffb3 | |||
| 577b8bc220 | |||
| 971461accf | |||
| 744f254c09 | |||
| 1584be89e2 | |||
| cdf563cbe9 | |||
| 872df98a2c | |||
| 7c13c96622 | |||
| 0c916b50c6 | |||
| 9ab6cc64a1 | |||
| 40c557f041 | |||
| a324841b7d | |||
| a29ef8e0f7 | |||
| 16d555037f | |||
| ab98eab49c | |||
| eccffe927a | |||
| 83f5c7de63 | |||
| 67c33fd4ff | |||
| 6d7b4874ce | |||
| f4375c2349 | |||
| 52b1723137 | |||
| b7786e8690 | |||
| 938df9bfe0 | |||
| a3a5b45cf8 | |||
| 362318707e | |||
| 98031a20a1 | |||
| b3da911d51 | |||
| 967e470e39 | |||
| ace7220752 | |||
| bdcf2bdc5f | |||
| 57bc617d26 | |||
| 17befc867e | |||
| d3154ceb1b | |||
| fe600790e8 | |||
| 11736501ec | |||
| e96131d722 | |||
| 99be51467e | |||
| d26784fb7f | |||
| 02424875fb | |||
| 0e2b3f2534 | |||
| b6ac34a52d | |||
| a37a639be1 | |||
| 1c14eec3cd | |||
| cc8925f88a | |||
| a20055f9d3 | |||
| 313cc189e9 | |||
| e0b0fa3d7e | |||
| 80f57c5351 | |||
| 306b0762a1 | |||
| 6bc5173236 | |||
| be0c394ac4 | |||
| 850781a38d | |||
| b032de9d1c | |||
| 4a4a736eb9 | |||
| 383ffb0bf8 | |||
| 3d03d905a1 | |||
| 4f0b6e219c | |||
| b164e51965 | |||
| 0dce7e7017 | |||
| 9f680dfd89 | |||
| 685ef5260a | |||
| 2826e3f9cd | |||
| 848381e098 | |||
| 99ab020347 | |||
| 65469cf1d2 | |||
| 0c9faece92 | |||
| d0171daf8e | |||
| 16baf12094 | |||
| c5a4c26b1e | |||
| 89acb0a914 | |||
| 21ad87d08d |
+1
-6
@@ -1,8 +1,3 @@
|
||||
.cursor/
|
||||
.cursor/
|
||||
tools/*.log
|
||||
*.log
|
||||
*.bak
|
||||
Logs/
|
||||
sac-spool/
|
||||
login_monitor.settings.ps1
|
||||
exchange_monitor.settings.ps1
|
||||
|
||||
+24
-106
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Доставка Login_Monitor.ps1 с файловой шары по версии (домен: ПК и серверы).
|
||||
.DESCRIPTION
|
||||
@@ -260,25 +260,11 @@ function Copy-RdpMonitorDeployBundle {
|
||||
}
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsHasPlaceholderSecrets {
|
||||
param([string]$SettingsPath)
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
|
||||
|
||||
if ($c -match '(?m)^\s*\$SacUrl\s*=\s*[''"]https?://[^''"]*example\.com[^''"]*[''"]') { return $true }
|
||||
if ($c -match '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_CHANGE_ME[''"]') { return $true }
|
||||
if ($c -match '(?m)^\s*\$TelegramBotToken\s*=\s*[''"]YOUR_BOT_TOKEN[''"]') { return $true }
|
||||
if ($c -match '(?m)^\s*\$TelegramChatID\s*=\s*[''"]YOUR_CHAT_ID[''"]') { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsNeedsSacBootstrap {
|
||||
param([string]$SettingsPath)
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
|
||||
|
||||
if (Test-RdpMonitorSettingsHasPlaceholderSecrets -SettingsPath $SettingsPath) { return $true }
|
||||
|
||||
if ($c -notmatch '(?m)^\s*\$UseSAC\s*=') { return $true }
|
||||
if ($c -match '(?m)^\s*\$UseSAC\s*=\s*[''"]off[''"]') {
|
||||
if ($c -notmatch '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_[^''"]+[''"]') { return $true }
|
||||
@@ -299,24 +285,12 @@ function Test-RdpMonitorSettingsNeedsServerDisplayNameHint {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsDailyReportEnabledIsTrue {
|
||||
function Test-RdpMonitorSettingsNeedsDailyReportHint {
|
||||
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*\$DailyReportEnabled\s*=\s*\$(?:true)\b') { return $true }
|
||||
if ($c -match '(?m)^\s*\$DailyReportEnabled\s*=\s*1\s*(?:#.*)?$') { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsNeedsDailyReportHint {
|
||||
param([string]$SettingsPath)
|
||||
if (-not (Test-Path -LiteralPath $SettingsPath)) { return $true }
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
|
||||
if (Test-RdpMonitorSettingsDailyReportEnabledIsTrue -SettingsPath $SettingsPath) { return $false }
|
||||
if (Test-RdpMonitorSettingsHasInvalidDailyReportAssignment -SettingsPath $SettingsPath) { return $true }
|
||||
if ($c -match '(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=') { return $true }
|
||||
if ($c -match '(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=') { return $false }
|
||||
return $true
|
||||
}
|
||||
|
||||
@@ -353,7 +327,7 @@ function Repair-RdpMonitorSettingsDailyReportAssignmentIfInvalid {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Sync-RdpMonitorSettingsDailyReportEnabledToTrue {
|
||||
function Update-RdpMonitorSettingsDailyReportHintIfMissing {
|
||||
param([string]$LocalSettings)
|
||||
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
||||
if (-not (Test-RdpMonitorSettingsNeedsDailyReportHint -SettingsPath $LocalSettings)) {
|
||||
@@ -363,35 +337,24 @@ function Sync-RdpMonitorSettingsDailyReportEnabledToTrue {
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $LocalSettings
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { return $false }
|
||||
|
||||
$dailyLine = '$DailyReportEnabled = $true # отчёт с агента (report.daily.rdp); $false или 0 — только SAC'
|
||||
$hintBlock = @(
|
||||
'# --- Суточный отчёт report.daily.rdp (агент или только SAC) ---'
|
||||
$dailyLine
|
||||
'# $DailyReportEnabled = $false # по умолчанию: только SAC; $true или 1 — отчёт с агента'
|
||||
'# Не пишите "= false" без $ — PowerShell воспримет false как команду.'
|
||||
) -join "`r`n"
|
||||
|
||||
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||
Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force
|
||||
Write-DeployLog "Добавление закомментированного `$DailyReportEnabled в login_monitor.settings.ps1; резервная копия: $bak"
|
||||
|
||||
if ($c -match '(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=') {
|
||||
Write-DeployLog "login_monitor.settings.ps1: `$DailyReportEnabled → `$true (резервная копия: $bak)"
|
||||
$newContent = [regex]::Replace(
|
||||
$c,
|
||||
'(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=.*$',
|
||||
$dailyLine,
|
||||
1
|
||||
)
|
||||
$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 {
|
||||
Write-DeployLog "login_monitor.settings.ps1: добавлен `$DailyReportEnabled = `$true (резервная копия: $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")
|
||||
}
|
||||
$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: добавлена подсказка # $DailyReportEnabled = $false'
|
||||
return $true
|
||||
}
|
||||
|
||||
@@ -402,7 +365,7 @@ function Sync-RdpMonitorSettingsDailyReportPatches {
|
||||
if (Repair-RdpMonitorSettingsDailyReportAssignmentIfInvalid -LocalSettings $LocalSettings) {
|
||||
$changed = $true
|
||||
}
|
||||
if (Sync-RdpMonitorSettingsDailyReportEnabledToTrue -LocalSettings $LocalSettings) {
|
||||
if (Update-RdpMonitorSettingsDailyReportHintIfMissing -LocalSettings $LocalSettings) {
|
||||
$changed = $true
|
||||
}
|
||||
return $changed
|
||||
@@ -810,7 +773,6 @@ function Invoke-RdpMonitorSettingsPostPatches {
|
||||
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
||||
$changed = $false
|
||||
if (Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Update-RdpMonitorSettingsServerIPv4HintIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Sync-RdpMonitorSettingsDailyReportPatches -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Sync-RdpMonitorSettingsExchangeNoisePatches -LocalSettings $LocalSettings) { $changed = $true }
|
||||
if (Repair-RdpMonitorSettingsWinRmLinesIfInvalid -LocalSettings $LocalSettings) { $changed = $true }
|
||||
@@ -924,6 +886,8 @@ function Update-RdpMonitorSettingsServerDisplayNameHintIfMissing {
|
||||
$hintBlock = @(
|
||||
'# --- Подпись сервера в Telegram и SAC (host.display_name); раскомментируйте при необходимости ---'
|
||||
"# `$ServerDisplayName = '$hostLabel'"
|
||||
'# --- Явный IPv4 хоста для SAC (опционально; иначе автоопределение) ---'
|
||||
'# $ServerIPv4 = '''
|
||||
) -join "`r`n"
|
||||
|
||||
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||
@@ -941,51 +905,6 @@ function Update-RdpMonitorSettingsServerDisplayNameHintIfMissing {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Test-RdpMonitorSettingsNeedsServerIPv4Hint {
|
||||
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*)?\$ServerIPv4\s*=') { return $false }
|
||||
return $true
|
||||
}
|
||||
|
||||
function Update-RdpMonitorSettingsServerIPv4HintIfMissing {
|
||||
param([string]$LocalSettings)
|
||||
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
||||
if (-not (Test-RdpMonitorSettingsNeedsServerIPv4Hint -SettingsPath $LocalSettings)) {
|
||||
return $false
|
||||
}
|
||||
|
||||
$c = Get-RdpMonitorSettingsRaw -Path $LocalSettings
|
||||
if ([string]::IsNullOrWhiteSpace($c)) { return $false }
|
||||
|
||||
$hintBlock = @(
|
||||
'# --- Явный IPv4 хоста для SAC (опционально; иначе автоопределение) ---'
|
||||
"# `$ServerIPv4 = '10.0.0.10'"
|
||||
) -join "`r`n"
|
||||
|
||||
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||
Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force
|
||||
Write-DeployLog "Добавление закомментированного `$ServerIPv4 в login_monitor.settings.ps1; резервная копия: $bak"
|
||||
|
||||
$insertAfterDisplay = '(?m)^(\s*(\#\s*)?\$ServerDisplayName\s*=.*)$'
|
||||
if ($c -match $insertAfterDisplay) {
|
||||
$newContent = [regex]::Replace($c, $insertAfterDisplay, ('$1' + "`r`n" + $hintBlock), 1)
|
||||
} else {
|
||||
$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: добавлена подсказка # `$ServerIPv4 = '10.0.0.10'"
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-RdpMonitorSacBlockFromExample {
|
||||
param([string]$ExamplePath)
|
||||
if (-not (Test-Path -LiteralPath $ExamplePath)) { return $null }
|
||||
@@ -994,8 +913,12 @@ function Get-RdpMonitorSacBlockFromExample {
|
||||
if ($ex -match '(?ms)(#\s*---\s*Security Alert Center.*?)(?=\r?\n#\s*---|\z)') {
|
||||
return $Matches[1].TrimEnd()
|
||||
}
|
||||
Write-DeployLog "Предупреждение: в example нет блока Security Alert Center — patch SAC пропущен."
|
||||
return $null
|
||||
return @(
|
||||
'# --- Security Alert Center (SAC) ---'
|
||||
'$UseSAC = ''fallback'''
|
||||
'$SacUrl = ''https://sac.example.com'''
|
||||
'$SacApiKey = ''sac_CHANGE_ME'''
|
||||
) -join "`r`n"
|
||||
}
|
||||
|
||||
function Sync-RdpMonitorUseSacFallbackMode {
|
||||
@@ -1033,10 +956,8 @@ function Update-RdpMonitorSettingsSacBlockIfMissing {
|
||||
|
||||
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[''"]') {
|
||||
if (-not (Test-RdpMonitorSettingsHasPlaceholderSecrets -SettingsPath $LocalSettings)) {
|
||||
Write-DeployLog "login_monitor.settings.ps1: блок SAC уже задан, файл не меняем."
|
||||
return $false
|
||||
}
|
||||
Write-DeployLog "login_monitor.settings.ps1: блок SAC уже задан, файл не меняем."
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1508,7 +1429,6 @@ try {
|
||||
|
||||
$needsSettingsBootstrap = Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $settingsLocal
|
||||
$needsDisplayNameHint = Test-RdpMonitorSettingsNeedsServerDisplayNameHint -SettingsPath $settingsLocal
|
||||
$needsServerIPv4Hint = Test-RdpMonitorSettingsNeedsServerIPv4Hint -SettingsPath $settingsLocal
|
||||
$needsDailyReportHint = Test-RdpMonitorSettingsNeedsDailyReportHint -SettingsPath $settingsLocal
|
||||
$needsDailyReportRepair = Test-RdpMonitorSettingsHasInvalidDailyReportAssignment -SettingsPath $settingsLocal
|
||||
$needsExchangeNoisePatch = Test-RdpMonitorSettingsNeedsExchangeNoisePatch -SettingsPath $settingsLocal
|
||||
@@ -1517,7 +1437,7 @@ try {
|
||||
$needsStartupRebootDetectMinutes = Test-RdpMonitorSettingsNeedsStartupRebootDetectMinutes -SettingsPath $settingsLocal
|
||||
$needsBundleSync = Test-RdpMonitorDeployBundleNeedsSync -ShareRoot $shareRoot
|
||||
$needsTaskExecutionLimitFix = Test-RdpMonitorDeployMainTaskNeedsUnlimitedExecutionTime -ShareRoot $shareRoot
|
||||
$needsSacBootstrap = $needsSettingsBootstrap -or $needsBundleSync -or $needsDisplayNameHint -or $needsServerIPv4Hint -or $needsDailyReportHint -or $needsDailyReportRepair -or $needsExchangeNoisePatch -or $needsWinRmInboundBlock -or $needsHeartbeatInterval -or $needsStartupRebootDetectMinutes -or $needsTaskExecutionLimitFix
|
||||
$needsSacBootstrap = $needsSettingsBootstrap -or $needsBundleSync -or $needsDisplayNameHint -or $needsDailyReportHint -or $needsDailyReportRepair -or $needsExchangeNoisePatch -or $needsWinRmInboundBlock -or $needsHeartbeatInterval -or $needsStartupRebootDetectMinutes -or $needsTaskExecutionLimitFix
|
||||
|
||||
if (Test-RdpMonitorExchangeServerRole) {
|
||||
Write-DeployLog "Обнаружена роль Exchange — при необходимости допишем WinRM/4624 noise settings в login_monitor.settings.ps1."
|
||||
@@ -1546,10 +1466,8 @@ try {
|
||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нужна донастройка SAC в settings — продолжаем деплой."
|
||||
} elseif ($needsDisplayNameHint) {
|
||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет подсказки `$ServerDisplayName в settings — продолжаем деплой."
|
||||
} elseif ($needsServerIPv4Hint) {
|
||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет подсказки `$ServerIPv4 в settings — продолжаем деплой."
|
||||
} elseif ($needsDailyReportHint) {
|
||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но `$DailyReportEnabled не `$true — допишем/исправим и продолжим деплой."
|
||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет `$DailyReportEnabled в settings — продолжаем деплой."
|
||||
} elseif ($needsDailyReportRepair) {
|
||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но в settings некорректно задан `$DailyReportEnabled (= true/false без `$) — продолжаем деплой."
|
||||
} elseif ($needsExchangeNoisePatch) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Диагностика RDP Login Monitor после входа по RDP (или при «тишине» в Telegram/SAC).
|
||||
.DESCRIPTION
|
||||
@@ -300,7 +300,6 @@ if (-not $isAdmin) {
|
||||
}
|
||||
|
||||
$since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack))
|
||||
$RcmLogName = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
|
||||
$isWs = Test-IsWorkstationOs
|
||||
[void]$report.AppendLine("OS ProductType workstation=$isWs (server mode LT 2/3/10, workstation LT 10)")
|
||||
[void]$report.AppendLine("Window StartTime >= $($since.ToString('yyyy-MM-dd HH:mm:ss'))")
|
||||
@@ -400,42 +399,8 @@ if ($notifyable.Count -eq 0 -and $recent4624.Count -gt 0) {
|
||||
|
||||
[void]$report.AppendLine('')
|
||||
[void]$report.AppendLine('UseSAC=exclusive: Telegram по rdp.login.success только из SAC (не локально агентом).')
|
||||
[void]$report.AppendLine('На сервере RDS без аудита Security 4624 — смотрите RCM Operational 1149 (2.1.5-SAC+: исправлен silent skip при ComputerName=-).')
|
||||
[void]$report.AppendLine('rdp.login.success = severity info; при SAC min_severity=warning Telegram не уйдёт, но событие в UI SAC должно быть.')
|
||||
|
||||
[void]$report.Append((Write-Section '10. RCM Operational 1149'))
|
||||
$recent1149 = @()
|
||||
try {
|
||||
$recent1149 = @(Get-WinEvent -FilterHashtable @{
|
||||
LogName = $RcmLogName
|
||||
ID = 1149
|
||||
StartTime = $since
|
||||
} -ErrorAction SilentlyContinue)
|
||||
} catch { }
|
||||
[void]$report.AppendLine("RCM 1149 в окне ($RcmLogName): $($recent1149.Count)")
|
||||
foreach ($ev in $recent1149 | Select-Object -First 8) {
|
||||
$u = '-'; $ip = '-'
|
||||
try {
|
||||
if ($ev.Properties.Count -gt 0) { $u = [string]$ev.Properties[0].Value }
|
||||
if ($ev.Properties.Count -gt 2) { $ip = [string]$ev.Properties[2].Value }
|
||||
} catch { }
|
||||
[void]$report.AppendLine(" $($ev.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')) User=$u IP=$ip")
|
||||
}
|
||||
$rcmNotifyLines = @()
|
||||
if (Test-Path -LiteralPath $monLog) {
|
||||
$sinceLog = $since.ToString('yyyy-MM-dd HH:mm')
|
||||
$rcmNotifyLines = @(Select-String -LiteralPath $monLog -Pattern 'Notify RCM 1149|Skip 1149' -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Line -ge $sinceLog } |
|
||||
Select-Object -ExpandProperty Line)
|
||||
}
|
||||
[void]$report.AppendLine("Строки Notify/Skip 1149 в login_monitor.log: $($rcmNotifyLines.Count)")
|
||||
foreach ($ln in $rcmNotifyLines | Select-Object -Last 10) { [void]$report.AppendLine(" $ln") }
|
||||
if ($recent1149.Count -gt 0 -and $rcmNotifyLines.Count -eq 0) {
|
||||
[void]$report.AppendLine('ВНИМАНИЕ: 1149 в журнале есть, в логе агента нет Notify/Skip — вероятен баг 2.1.4 (все 1149 отбрасывались) или агент не работал в момент входа.')
|
||||
}
|
||||
|
||||
[void]$report.AppendLine('')
|
||||
[void]$report.AppendLine('Проверьте SAC: type=rdp.login.success, hostname, время входа, event_id из login_monitor.log.')
|
||||
[void]$report.AppendLine('Проверьте SAC: type=rdp.login.success, hostname ITIS198, время входа, event_id из login_monitor.log.')
|
||||
|
||||
$text = $report.ToString()
|
||||
[System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true))
|
||||
|
||||
@@ -2,38 +2,13 @@
|
||||
|
||||
Скрипт **`update-rdp-monitor.ps1`** на сервере публикации (например DC3) выполняет `git pull` и копирует файлы в шару.
|
||||
|
||||
## Путь на шаре (`-NetlogonDest`)
|
||||
|
||||
После `git pull` скрипт копирует дистрибутив в UNC-каталог NETLOGON:
|
||||
|
||||
```text
|
||||
\\<имя-DC>\NETLOGON\RDP-login-monitor
|
||||
```
|
||||
|
||||
`<имя-DC>` — NetBIOS-имя или FQDN **контроллера домена**, где лежит SYSVOL (тот же хост, с которого GPO запускает `Deploy-LoginMonitor.ps1`). Примеры:
|
||||
|
||||
- `\\K6A-DC3\NETLOGON\RDP-login-monitor`
|
||||
- `\\dc01.corp.example.com\NETLOGON\RDP-login-monitor`
|
||||
|
||||
Значение по умолчанию в скрипте — **заглушка** `\\dc.contoso.local\NETLOGON\RDP-login-monitor`. В реальном домене её **нужно переопределить**, иначе после успешного `git pull` будет ошибка **«Не найден сетевой путь»** (скрипт не достучится до несуществующего хоста).
|
||||
|
||||
Проверка перед публикацией:
|
||||
|
||||
```powershell
|
||||
Test-Path '\\K6A-DC3\NETLOGON\RDP-login-monitor'
|
||||
# или хотя бы корень шары:
|
||||
Test-Path '\\K6A-DC3\NETLOGON'
|
||||
```
|
||||
|
||||
Должно вернуть `True` под учётной записью, с которой запускаете публикацию (на DC — обычно локальный админ; с рабочей станции — доменный админ с доступом к NETLOGON).
|
||||
|
||||
## Параметры по умолчанию
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| `$RepoPath` | `C:\Soft\Git\RDP-login-monitor` |
|
||||
| `$NetlogonDest` | `\\dc.contoso.local\NETLOGON\RDP-login-monitor` *(заглушка — замените на свой DC)* |
|
||||
| `$GitUrl` | `https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git` |
|
||||
| `$NetlogonDest` | `\\dc.contoso.local\NETLOGON\RDP-login-monitor` |
|
||||
| `$GitUrl` | `https://github.com/PTah/RDP-login-monitor.git` |
|
||||
| `$GitBranch` | `main` |
|
||||
| `$LogFile` | `C:\soft\Logs\update-rdp-monitor.log` |
|
||||
|
||||
@@ -53,27 +28,9 @@ Test-Path '\\K6A-DC3\NETLOGON'
|
||||
|
||||
## Запуск
|
||||
|
||||
**На сервере публикации** (клон репозитория + доступ к NETLOGON):
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Soft\Git\RDP-login-monitor\update-rdp-monitor.ps1 `
|
||||
-NetlogonDest '\\K6A-DC3\NETLOGON\RDP-login-monitor'
|
||||
```
|
||||
|
||||
Другой remote git (закрытое зеркало):
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Soft\Git\RDP-login-monitor\update-rdp-monitor.ps1 `
|
||||
-NetlogonDest '\\K6A-DC3\NETLOGON\RDP-login-monitor' `
|
||||
-GitUrl 'https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git'
|
||||
```
|
||||
|
||||
Пробный прогон без копирования:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Soft\Git\RDP-login-monitor\update-rdp-monitor.ps1 `
|
||||
-NetlogonDest '\\K6A-DC3\NETLOGON\RDP-login-monitor' `
|
||||
-WhatIf
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1 -WhatIf
|
||||
```
|
||||
|
||||
После pull обязательно проверьте **`version.txt`** на шаре — его номер определяет, подтянут ли обновления на клиентах. Метка может включать суффикс (например **`1.2.27-SAC`**); **`Deploy-LoginMonitor.ps1`** сравнивает её с **`deployed_version.txt`** по полной строке.
|
||||
|
||||
@@ -216,7 +216,7 @@ powershell.exe -NoProfile -ExecutionPolicy Bypass `
|
||||
|
||||
**Требования:** domain admin в SAC; `SAC_PUBLIC_URL` доступен с ПК; `rdp_git_repo_url` в настройках обновлений. **`Deploy-LoginMonitor.ps1` на шаре NETLOGON для этого пути не обязателен** — скрипт приходит в zip.
|
||||
|
||||
Лог операции — в модалке SAC сразу при старте. См. [agent-control-plane.md](https://git.papatramp.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-control-plane.md) §4.3.
|
||||
Лог операции — в модалке SAC сразу при старте. См. [agent-control-plane.md](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-control-plane.md) §4.3.
|
||||
|
||||
## Версии
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Exchange Mail Security — руководство
|
||||
# Exchange Mail Security — руководство
|
||||
|
||||
Скрипт **`Exchange-MailSecurity.ps1`** предназначен **только для сервера Microsoft Exchange** с Exchange Management Shell (EMS). Не устанавливается на все компьютеры домена через GPO RDP-монитора.
|
||||
|
||||
@@ -68,16 +68,6 @@
|
||||
- Каналы: **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`** — не слать «всё чисто» при нуле находок.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1.
|
||||
# Запуск: от администратора на ТОМ ЖЕ компьютере, где будет Login_Monitor.ps1.
|
||||
# Результат (Base64) вставьте в login_monitor.settings.ps1 или exchange_monitor.settings.ps1:
|
||||
# $TelegramBotTokenProtectedB64 / $TelegramChatIDProtectedB64 / $MailSmtpPasswordProtectedB64.
|
||||
param(
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Мониторинг Exchange: очереди транспорта, пересылка на внешние адреса (Inbox rules + mailbox forwarding + transport rules).
|
||||
.DESCRIPTION
|
||||
@@ -10,13 +10,12 @@
|
||||
Опционально: exchange_monitor.settings.ps1 в том же каталоге (секреты, whitelist).
|
||||
#>
|
||||
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[ValidateSet('Queues', 'Inbox', 'Watchdog')]
|
||||
[string]$Mode = 'Queues',
|
||||
[switch]$InstallTasks,
|
||||
[switch]$Watchdog,
|
||||
[switch]$WhatIf
|
||||
[switch]$Watchdog
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
@@ -26,7 +25,7 @@ $ErrorActionPreference = 'Stop'
|
||||
# КОНФИГУРАЦИЯ
|
||||
# ============================================
|
||||
|
||||
$ScriptVersion = '1.6.8'
|
||||
$ScriptVersion = '1.6.7'
|
||||
$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'
|
||||
@@ -101,18 +100,6 @@ 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
|
||||
@@ -248,7 +235,6 @@ if ($InstallTasks) {
|
||||
Send-ExchangeInstallNotification
|
||||
Write-ExchLog 'InstallTasks: install notification sent'
|
||||
}
|
||||
Write-ExchLog 'InstallTasks: перед первым ночным Inbox-сканом выполните: Exchange-MailSecurity.ps1 -Mode Inbox -WhatIf'
|
||||
exit 0
|
||||
}
|
||||
|
||||
@@ -532,15 +518,6 @@ 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
|
||||
@@ -783,20 +760,6 @@ 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 ', ')"
|
||||
@@ -916,8 +879,7 @@ if (-not (Test-RunningElevated)) {
|
||||
Write-ExchLog 'WARNING: not running elevated - EMS/tasks may fail.'
|
||||
}
|
||||
|
||||
Write-ExchLog "=== Exchange-MailSecurity v$ScriptVersion Mode=$Mode$(if ($WhatIf) { ' WhatIf' }) ==="
|
||||
Write-ExchangeScanSafetyWarnings
|
||||
Write-ExchLog "=== Exchange-MailSecurity v$ScriptVersion Mode=$Mode ==="
|
||||
|
||||
try {
|
||||
switch ($Mode) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#Requires -RunAsAdministrator
|
||||
#Requires -RunAsAdministrator
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$TaskName = "RDP-Login-Monitor-Deploy",
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
#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
|
||||
+27
-391
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Мониторинг логинов/попыток входа с уведомлениями в Telegram
|
||||
.DESCRIPTION
|
||||
@@ -16,7 +16,7 @@
|
||||
- Добавлены исключения шума: DWM-*, UMFD-*, HealthMailbox*, Font Driver Host*, NtLmSsp и др.
|
||||
- Heartbeat без дрейфа: используется nextHeartbeatTime (а не "прошло N секунд").
|
||||
- Win32_OperatingSystem.ProductType: 1 = рабочая станция (4624/4625 только LogonType 10 + при наличии журнала событие 1149 RCM);
|
||||
2/3 = сервер/КД — Security 4624/4625 (типы 2, 3, 10) + при наличии журнала RCM — 1149 (на RDS часто нет 4624 без аудита входа).
|
||||
2/3 = сервер/КД — прежняя логика (типы 2, 3, 10).
|
||||
Секреты Telegram (DPAPI LocalMachine): на ЭТОЙ машине, под админом, выполните:
|
||||
Add-Type -AssemblyName System.Security
|
||||
$p=[Text.Encoding]::UTF8.GetBytes('<токен>')
|
||||
@@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15
|
||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||
$ScriptVersion = "2.1.15-SAC"
|
||||
$ScriptVersion = "2.1.2-SAC"
|
||||
|
||||
# Логи (все под InstallRoot)
|
||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||
@@ -115,7 +115,6 @@ $LoginSuccessNotifyDedupSeconds = 90
|
||||
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
||||
$GatewayPollCursorFile = Join-Path $script:InstallRoot "Logs\last_rdgateway_poll.txt"
|
||||
$SecurityPollCursorFile = Join-Path $script:InstallRoot "Logs\last_security_poll.txt"
|
||||
$Rcm1149PollCursorFile = Join-Path $script:InstallRoot "Logs\last_rcm1149_poll.txt"
|
||||
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
|
||||
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
||||
$script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst"
|
||||
@@ -135,17 +134,12 @@ $LastReportFile = Join-Path $script:InstallRoot "Logs\last_daily_report.txt"
|
||||
$EnableRDGatewayMonitoring = $true
|
||||
$RDGatewayLogName = "Microsoft-Windows-TerminalServices-Gateway/Operational"
|
||||
$RDGatewayEvents = @(302, 303)
|
||||
# Дубликаты RDG 302 (User+Target+ExternalIP) в окне N с — один alert в SAC/Telegram.
|
||||
$Rdg302NotifyDedupSeconds = 90
|
||||
# Макс. возраст сохранённого курсора RD Gateway (мин): если файл старше — replay не делаем.
|
||||
$GatewayEventsLookbackMinutes = 60
|
||||
# Security 4624/4625/4648: персистентный cursor + replay не старше N мин (медленный boot / поздний старт агента).
|
||||
$SecurityEventsLookbackMinutes = 60
|
||||
# RCM 1149: персистентный cursor + replay (как Security).
|
||||
$Rcm1149EventsLookbackMinutes = 60
|
||||
|
||||
# RDP Remote Connection Manager: User authentication succeeded — событие 1149 (workstation + RDS server)
|
||||
$EnableRcm1149Monitoring = 1
|
||||
# RDP Remote Connection Manager (workstations): User authentication succeeded — событие 1149
|
||||
$RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational"
|
||||
$RcmEventId = 1149
|
||||
|
||||
@@ -258,9 +252,7 @@ $SacUrl = ''
|
||||
$SacApiKey = ''
|
||||
$SacSpoolDir = ''
|
||||
$SacAgentIdFile = ''
|
||||
$SacTimeoutSec = 45
|
||||
$SacSpoolFlushMaxFiles = 50
|
||||
$SacSpoolMaxAgeHours = 72
|
||||
$SacTimeoutSec = 12
|
||||
$SacTlsSkipVerify = $false
|
||||
$SacFallbackFailures = 5
|
||||
|
||||
@@ -1140,9 +1132,6 @@ 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)."
|
||||
}
|
||||
@@ -1539,122 +1528,6 @@ function Test-RcmLogAvailable {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-Rcm1149UserDataEventInfoMap {
|
||||
param($Event)
|
||||
|
||||
$map = @{}
|
||||
try {
|
||||
$xml = [xml]$Event.ToXml()
|
||||
$userData = $xml.Event.UserData
|
||||
if ($null -eq $userData) { return $map }
|
||||
|
||||
$eventXml = $userData.EventXML
|
||||
if ($null -eq $eventXml) {
|
||||
foreach ($child in @($userData.ChildNodes)) {
|
||||
if ($null -ne $child -and $child.LocalName -eq 'EventXML') {
|
||||
$eventXml = $child
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($null -eq $eventXml) { return $map }
|
||||
|
||||
foreach ($node in @($eventXml.ChildNodes)) {
|
||||
if ($null -eq $node -or $node.NodeType -ne [System.Xml.XmlNodeType]::Element) { continue }
|
||||
$map[$node.LocalName] = [string]$node.InnerText
|
||||
}
|
||||
} catch { }
|
||||
return $map
|
||||
}
|
||||
|
||||
function Resolve-Rcm1149UsernameFromParts {
|
||||
param(
|
||||
[string]$Param1,
|
||||
[string]$Param2
|
||||
)
|
||||
|
||||
$p1 = if ($null -ne $Param1) { $Param1.Trim() } else { '' }
|
||||
$p2 = if ($null -ne $Param2) { $Param2.Trim() } else { '' }
|
||||
if ([string]::IsNullOrWhiteSpace($p1)) { return $null }
|
||||
if ($p1 -match '[\\@]') { return $p1 }
|
||||
if (-not [string]::IsNullOrWhiteSpace($p2)) {
|
||||
return "$p2\$p1"
|
||||
}
|
||||
return $p1
|
||||
}
|
||||
|
||||
function Get-Rcm1149UsernameFromQwinsta {
|
||||
<#
|
||||
На части ПК (наблюдалось на Win10 Pro) RCM 1149 пишет пустые Param1/Param2,
|
||||
хотя сеанс уже есть в qwinsta. Берём единственную RDP-сессию (rdp-tcp#N / Disc).
|
||||
Состояния локализованы (Активно/Listen/…) — не завязываемся на английский STATE.
|
||||
#>
|
||||
$qwExe = Join-Path $env:SystemRoot 'System32\qwinsta.exe'
|
||||
if (-not (Test-Path -LiteralPath $qwExe)) { return $null }
|
||||
|
||||
$prevEa = $ErrorActionPreference
|
||||
$rawLines = @()
|
||||
try {
|
||||
$ErrorActionPreference = 'SilentlyContinue'
|
||||
$rawLines = @(& $qwExe 2>&1 | ForEach-Object { [string]$_ })
|
||||
} catch {
|
||||
return $null
|
||||
} finally {
|
||||
$ErrorActionPreference = $prevEa
|
||||
}
|
||||
|
||||
$candidates = [System.Collections.Generic.List[string]]::new()
|
||||
foreach ($raw in $rawLines) {
|
||||
$text = if ($null -ne $raw) { $raw.Trim() } else { '' }
|
||||
if ([string]::IsNullOrWhiteSpace($text)) { continue }
|
||||
if ($text -match '(?i)^SESSION') { continue }
|
||||
if ($text -match '^-+$') { continue }
|
||||
|
||||
$parts = @($text -split '\s+' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
|
||||
if ($parts.Count -lt 2) { continue }
|
||||
|
||||
$idIdx = -1
|
||||
$sid = -1
|
||||
for ($i = 0; $i -lt $parts.Count; $i++) {
|
||||
$token = $parts[$i].TrimStart('>')
|
||||
if ($token -match '^\d+$') {
|
||||
$idIdx = $i
|
||||
$sid = [int]$token
|
||||
break
|
||||
}
|
||||
}
|
||||
if ($idIdx -lt 1) { continue }
|
||||
# Listener «rdp-tcp» / ID 65536 — не пользовательская сессия.
|
||||
if ($sid -eq 65536) { continue }
|
||||
|
||||
$before = @($parts[0..($idIdx - 1)])
|
||||
if ($before.Count -eq 1) {
|
||||
$sessionName = ''
|
||||
$userName = $before[0].TrimStart('>')
|
||||
} else {
|
||||
$sessionName = $before[0].TrimStart('>')
|
||||
$userName = $before[1]
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($userName)) { continue }
|
||||
if ($userName -match '(?i)^(services|console|rdp-tcp)$') { continue }
|
||||
if ($sessionName -match '(?i)^console$') { continue }
|
||||
if ($sessionName -match '(?i)^rdp-tcp$' -and $sessionName -notmatch '#') { continue }
|
||||
|
||||
$isRdpNamed = $sessionName -match '(?i)^rdp-tcp#\d+'
|
||||
$isDiscBare = [string]::IsNullOrWhiteSpace($sessionName)
|
||||
if (-not ($isRdpNamed -or $isDiscBare)) { continue }
|
||||
|
||||
if (-not $candidates.Contains($userName)) {
|
||||
[void]$candidates.Add($userName)
|
||||
}
|
||||
}
|
||||
|
||||
if ($candidates.Count -eq 1) {
|
||||
return $candidates[0]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-Rcm1149EventInfo {
|
||||
param($Event)
|
||||
$eventData = @{
|
||||
@@ -1664,49 +1537,25 @@ function Get-Rcm1149EventInfo {
|
||||
}
|
||||
try {
|
||||
$map = Get-EventDataMap -Event $Event
|
||||
$userDataMap = Get-Rcm1149UserDataEventInfoMap -Event $Event
|
||||
$user = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||
"TargetUser","User","Domain User","Param1","AccountName","ConnectionUser","SubjectUserName"
|
||||
)
|
||||
$ip = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||
"ClientIP","Client Address","IpAddress","Ip","Param3","Address","CallingStationId"
|
||||
)
|
||||
if ([string]::IsNullOrWhiteSpace($user)) {
|
||||
$user = Resolve-Rcm1149UsernameFromParts -Param1 $userDataMap['Param1'] -Param2 $userDataMap['Param2']
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($ip)) {
|
||||
$ip = Get-FirstNonEmptyMapValue -DataMap $userDataMap -Keys @('Param3', 'Param2', 'Param1')
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($user)) { $eventData.Username = [string]$user }
|
||||
if (-not [string]::IsNullOrWhiteSpace($ip)) {
|
||||
$eventData.ClientIP = Get-RdpMonitorNormalizedClientIp -Value ([string]$ip)
|
||||
}
|
||||
if (-not [string]::IsNullOrWhiteSpace($ip)) { $eventData.ClientIP = [string]$ip }
|
||||
|
||||
if (($eventData.Username -eq '-' -or [string]::IsNullOrWhiteSpace($eventData.Username)) -and $Event.Properties.Count -gt 0) {
|
||||
$p0 = [string]$Event.Properties[0].Value
|
||||
if (-not [string]::IsNullOrWhiteSpace($p0)) {
|
||||
$eventData.Username = $p0
|
||||
}
|
||||
if ($eventData.Username -eq '-' -and $Event.Properties.Count -gt 0) {
|
||||
$eventData.Username = [string]$Event.Properties[0].Value
|
||||
}
|
||||
if ($eventData.ClientIP -eq '-' -or [string]::IsNullOrWhiteSpace($eventData.ClientIP)) {
|
||||
if ($eventData.ClientIP -eq '-') {
|
||||
if ($Event.Properties.Count -gt 2) {
|
||||
$p2 = [string]$Event.Properties[2].Value
|
||||
if (-not [string]::IsNullOrWhiteSpace($p2)) {
|
||||
$eventData.ClientIP = Get-RdpMonitorNormalizedClientIp -Value $p2
|
||||
}
|
||||
$eventData.ClientIP = [string]$Event.Properties[2].Value
|
||||
} elseif ($Event.Properties.Count -gt 1) {
|
||||
$p1 = [string]$Event.Properties[1].Value
|
||||
if (-not [string]::IsNullOrWhiteSpace($p1)) {
|
||||
$eventData.ClientIP = Get-RdpMonitorNormalizedClientIp -Value $p1
|
||||
}
|
||||
$eventData.ClientIP = [string]$Event.Properties[1].Value
|
||||
}
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($eventData.Username) -or $eventData.Username -eq '-') {
|
||||
$eventData.Username = '-'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($eventData.ClientIP) -or $eventData.ClientIP -eq '-') {
|
||||
$eventData.ClientIP = '-'
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Ошибка разбора события RCM 1149: $($_.Exception.Message)"
|
||||
}
|
||||
@@ -2788,7 +2637,7 @@ function Send-Heartbeat {
|
||||
if ($script:IsWorkstation) {
|
||||
$message += "`r`n📌 <b>Режим:</b> рабочая станция — Security 4624/4625 только LogonType 10 (RDP); при наличии журнала — также 1149 (Remote Connection Manager)."
|
||||
} else {
|
||||
$message += "`r`n📌 <b>Режим:</b> сервер — Security 4624/4625, типы входа 2, 3, 10; при журнале RCM — также 1149 (RDS без аудита 4624)."
|
||||
$message += "`r`n📌 <b>Режим:</b> сервер — Security 4624/4625, типы входа 2, 3, 10."
|
||||
}
|
||||
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
|
||||
if ($ignoreEntries.Count -gt 0) {
|
||||
@@ -3179,13 +3028,10 @@ function Parse-RdpMonitorIgnoreListLine {
|
||||
if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null }
|
||||
if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) }
|
||||
|
||||
$scopes = @('4624', '4625', '4634', '4647')
|
||||
$scopes = @('4624', '4625')
|
||||
if ($line -match '^(?i)(4740|lockout|блокир)\s*:\s*(.+)$') {
|
||||
$scopes = @('4740')
|
||||
$line = $Matches[2].Trim()
|
||||
} elseif ($line -match '^(?i)(logoff|logout|4634|4647)\s*:\s*(.+)$') {
|
||||
$scopes = @('4634', '4647')
|
||||
$line = $Matches[2].Trim()
|
||||
} elseif ($line -match '^(?i)(shadow|20506)\s*:\s*(.+)$') {
|
||||
$scopes = @('20506', '20507', '20510')
|
||||
$line = $Matches[2].Trim()
|
||||
@@ -3196,7 +3042,7 @@ function Parse-RdpMonitorIgnoreListLine {
|
||||
$scopes = @('5140')
|
||||
$line = $Matches[2].Trim()
|
||||
} elseif ($line -match '^(?i)(all|\*)\s*:\s*(.+)$') {
|
||||
$scopes = @('4624', '4625', '4634', '4647', '4740', '20506', '20507', '20510', 'winrm', '5140')
|
||||
$scopes = @('4624', '4625', '4740', '20506', '20507', '20510', 'winrm', '5140')
|
||||
$line = $Matches[2].Trim()
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($line)) { return $null }
|
||||
@@ -3381,10 +3227,8 @@ function Should-IgnoreEvent {
|
||||
if ($Username -like $p) { return $true }
|
||||
}
|
||||
|
||||
if ($EventID -ne 1149 -and $EventID -notin 4634, 4647) {
|
||||
if ($ComputerName -eq "Authz" -or $ComputerName -like "Authz*") { return $true }
|
||||
if ($ComputerName -eq "NtLmSsp" -or $ComputerName -like "NtLmSsp*" -or $ComputerName -like "*NtLmSsp*") { return $true }
|
||||
}
|
||||
if ($ComputerName -eq "Authz" -or $ComputerName -like "Authz*") { return $true }
|
||||
if ($ComputerName -eq "NtLmSsp" -or $ComputerName -like "NtLmSsp*" -or $ComputerName -like "*NtLmSsp*") { return $true }
|
||||
|
||||
foreach ($lp in $ExcludedLogonProcesses) {
|
||||
if ($ProcessName -like "*$lp*") { return $true }
|
||||
@@ -3394,14 +3238,13 @@ function Should-IgnoreEvent {
|
||||
}
|
||||
|
||||
if ($SourceIP -eq "127.0.0.1" -or $SourceIP -like "fe80:*") { return $true }
|
||||
# RCM 1149 и logoff 4634/4647 часто без WorkstationName в EventData.
|
||||
if ($EventID -notin 1149, 4634, 4647 -and ($ComputerName -eq "-" -or $ComputerName -eq "N/A")) { return $true }
|
||||
if ($ComputerName -eq "-" -or $ComputerName -eq "N/A") { return $true }
|
||||
|
||||
foreach ($pattern in $ExcludedComputerPatterns) {
|
||||
if ($ComputerName -like $pattern) { return $true }
|
||||
}
|
||||
|
||||
if ($EventID -in 4624, 4625, 4634, 4647, 1149) {
|
||||
if ($EventID -in 4624, 4625) {
|
||||
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username `
|
||||
-ComputerName $ComputerName -SourceIP $SourceIP) {
|
||||
return $true
|
||||
@@ -3421,7 +3264,6 @@ function Get-LoginEventInfo {
|
||||
SourceIP = "-"
|
||||
ProcessName = "-"
|
||||
LogonType = 0
|
||||
SessionId = $null
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -3439,17 +3281,6 @@ function Get-LoginEventInfo {
|
||||
$eventData.ProcessName = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||
"SubStatus","Status","FailureReason","FailureReasonCode"
|
||||
)
|
||||
} elseif ($Event.Id -in 4634, 4647) {
|
||||
if ($Event.Id -eq 4647) {
|
||||
$subjectUser = Get-FirstNonEmptyMapValue -DataMap $map -Keys @("SubjectUserName","AccountName")
|
||||
if (-not [string]::IsNullOrWhiteSpace($subjectUser)) {
|
||||
$eventData.Username = $subjectUser
|
||||
}
|
||||
}
|
||||
$sidRaw = Get-FirstNonEmptyMapValue -DataMap $map -Keys @("TargetLogonId","SubjectLogonId","LogonId")
|
||||
if (-not [string]::IsNullOrWhiteSpace($sidRaw)) {
|
||||
$eventData.SessionId = [string]$sidRaw.Trim()
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
Write-Log "Ошибка при извлечении данных события: $($_.Exception.Message)"
|
||||
@@ -3522,8 +3353,6 @@ function Format-LoginEvent {
|
||||
$message = "<b>"
|
||||
if ($EventID -eq 4624) { $message += "✅ УСПЕШНЫЙ ВХОД" }
|
||||
elseif ($EventID -eq 4625) { $message += "❌ НЕУДАЧНАЯ ПОПЫТКА" }
|
||||
elseif ($EventID -eq 4634) { $message += "🚪 ВЫХОД ИЗ СЕССИИ" }
|
||||
elseif ($EventID -eq 4647) { $message += "🚪 ВЫХОД ПОЛЬЗОВАТЕЛЯ" }
|
||||
else { $message += "⚠️ СОБЫТИЕ" }
|
||||
$message += "</b>`r`n"
|
||||
|
||||
@@ -3544,7 +3373,6 @@ function Format-LoginEvent {
|
||||
|
||||
$script:FailedLogonBuckets = @{}
|
||||
$script:LoginSuccessNotifyDedup = @{}
|
||||
$script:LogoffNotifyDedup = @{}
|
||||
|
||||
function Get-RdpLoginNotifyDedupHostPart {
|
||||
param([string]$SecurityLogComputerName)
|
||||
@@ -3603,33 +3431,6 @@ function Test-RdpLoginSuccessNotifyDedupAllow {
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-RdpLogoffNotifyDedupKey {
|
||||
param(
|
||||
[string]$SecurityLogComputerName,
|
||||
[string]$Username,
|
||||
[int]$LogonType
|
||||
)
|
||||
$hostPart = Get-RdpLoginNotifyDedupHostPart -SecurityLogComputerName $SecurityLogComputerName
|
||||
$userPart = Get-RdpLoginNotifyDedupUsernamePart -Username $Username
|
||||
return "$hostPart|logoff|$userPart|$LogonType"
|
||||
}
|
||||
|
||||
function Test-RdpLogoffNotifyDedupAllow {
|
||||
param([string]$DedupKey)
|
||||
|
||||
if ($LoginSuccessNotifyDedupSeconds -le 0) { return $true }
|
||||
$nowUtc = (Get-Date).ToUniversalTime()
|
||||
if ($script:LogoffNotifyDedup.ContainsKey($DedupKey)) {
|
||||
$lastUtc = $script:LogoffNotifyDedup[$DedupKey]
|
||||
$delta = ($nowUtc - $lastUtc).TotalSeconds
|
||||
if ($delta -ge 0 -and $delta -lt $LoginSuccessNotifyDedupSeconds) {
|
||||
return $false
|
||||
}
|
||||
}
|
||||
$script:LogoffNotifyDedup[$DedupKey] = $nowUtc
|
||||
return $true
|
||||
}
|
||||
|
||||
function Get-FailedLogonSourceKeyPart {
|
||||
param(
|
||||
[string]$SourceIP,
|
||||
@@ -3955,51 +3756,6 @@ function Set-RdpSecurityPollCursor {
|
||||
}
|
||||
}
|
||||
|
||||
function Get-RdpRcm1149PollCursor {
|
||||
$now = Get-Date
|
||||
$fresh = $now.AddSeconds(-10)
|
||||
$maxAgeMin = [math]::Max(1, [int]$Rcm1149EventsLookbackMinutes)
|
||||
$lookbackFloor = $now.AddMinutes(-1 * $maxAgeMin)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Rcm1149PollCursorFile)) {
|
||||
Write-Log "RCM 1149: cursor отсутствует — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss')) (lookback ${maxAgeMin} мин)."
|
||||
return $lookbackFloor
|
||||
}
|
||||
try {
|
||||
$raw = (Get-Content -LiteralPath $Rcm1149PollCursorFile -TotalCount 1 -ErrorAction Stop).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($raw)) {
|
||||
Write-Log "RCM 1149: cursor пуст — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss')) (lookback ${maxAgeMin} мин)."
|
||||
return $lookbackFloor
|
||||
}
|
||||
$parsed = [datetime]::Parse($raw, [Globalization.CultureInfo]::InvariantCulture, [Globalization.DateTimeStyles]::RoundtripKind)
|
||||
if ($parsed.Kind -eq [DateTimeKind]::Utc) {
|
||||
$parsed = $parsed.ToLocalTime()
|
||||
}
|
||||
if ($parsed -lt $lookbackFloor) {
|
||||
Write-Log "RCM 1149: cursor старше $maxAgeMin мин — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss'))."
|
||||
return $lookbackFloor
|
||||
}
|
||||
Write-Log "RCM 1149: cursor=$($parsed.ToString('dd.MM.yyyy HH:mm:ss')) (lookback cap ${maxAgeMin} мин)."
|
||||
return $parsed
|
||||
} catch {
|
||||
Write-Log "RCM 1149: не удалось прочитать cursor ($Rcm1149PollCursorFile) — replay с $($lookbackFloor.ToString('dd.MM.yyyy HH:mm:ss'))."
|
||||
return $lookbackFloor
|
||||
}
|
||||
}
|
||||
|
||||
function Set-RdpRcm1149PollCursor {
|
||||
param([Parameter(Mandatory = $true)][datetime]$Cursor)
|
||||
try {
|
||||
$dir = Split-Path -Parent $Rcm1149PollCursorFile
|
||||
if (-not (Test-Path -LiteralPath $dir)) {
|
||||
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||
}
|
||||
Write-TextFileUtf8Bom -Path $Rcm1149PollCursorFile -Text ($Cursor.ToString('o'))
|
||||
} catch {
|
||||
Write-Log "WARN: RCM 1149 cursor save failed: $($_.Exception.Message)"
|
||||
}
|
||||
}
|
||||
|
||||
function Test-RdgGatewayBenignErrorCode {
|
||||
param([string]$ErrorCode)
|
||||
if ([string]::IsNullOrWhiteSpace($ErrorCode)) { return $true }
|
||||
@@ -4010,20 +3766,6 @@ function Test-RdgGatewayBenignErrorCode {
|
||||
return $false
|
||||
}
|
||||
|
||||
function Get-Rdg302NotifyDedupKey {
|
||||
param(
|
||||
[string]$Username,
|
||||
[string]$InternalIP,
|
||||
[string]$ExternalIP
|
||||
)
|
||||
$userPart = Get-RdpLoginNotifyDedupUsernamePart -Username $Username
|
||||
$target = if ($InternalIP) { [string]$InternalIP.Trim() } else { '-' }
|
||||
if ([string]::IsNullOrWhiteSpace($target)) { $target = '-' }
|
||||
$ext = if ($ExternalIP) { [string]$ExternalIP.Trim() } else { '-' }
|
||||
if ([string]::IsNullOrWhiteSpace($ext)) { $ext = '-' }
|
||||
return "rdg302|$userPart|$target|$ext"
|
||||
}
|
||||
|
||||
function Get-RdgGatewaySacEventType {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][int]$EventId,
|
||||
@@ -4586,7 +4328,7 @@ function Start-LoginMonitor {
|
||||
if ($osKind.IsWorkstation) {
|
||||
Write-Log "Режим рабочей станции: Security — только LogonType 10 (RDP); при наличии журнала — событие 1149 (Remote Connection Manager)."
|
||||
} else {
|
||||
Write-Log "Режим сервера: Security — LogonType 2, 3, 10; при журнале RCM — 1149 (если `$EnableRcm1149Monitoring)."
|
||||
Write-Log "Режим сервера: Security — LogonType 2, 3, 10"
|
||||
}
|
||||
if (Test-MonitorFeatureEnabled -Value $EnableAdminShareMonitoring) {
|
||||
Write-Log "Admin share: Security 5140 для $($AdminShareMonitorSuffixes -join ', ') (audit File Share; workstation + server)."
|
||||
@@ -4653,15 +4395,9 @@ function Start-LoginMonitor {
|
||||
$rdGatewayAvailable = $false
|
||||
if ($EnableRDGatewayMonitoring) { $rdGatewayAvailable = Test-RDGatewayLog }
|
||||
|
||||
$rcmMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableRcm1149Monitoring) -and (Test-RcmLogAvailable)
|
||||
if (Test-MonitorFeatureEnabled -Value $EnableRcm1149Monitoring) {
|
||||
if ($rcmMonitoringEnabled) {
|
||||
Write-Log "RCM 1149: опрос включён ($RcmLogName)."
|
||||
} elseif ($osKind.IsWorkstation) {
|
||||
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
|
||||
} else {
|
||||
Write-Log "Сервер: журнал Remote Connection Manager недоступен — RCM 1149 отключён; только Security 4624/4625."
|
||||
}
|
||||
$rcmMonitoringEnabled = ($osKind.IsWorkstation -and (Test-RcmLogAvailable))
|
||||
if ($osKind.IsWorkstation -and -not $rcmMonitoringEnabled) {
|
||||
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
|
||||
}
|
||||
|
||||
$rcmShadowMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableRcmShadowControlMonitoring) -and (Test-RcmLogAvailable)
|
||||
@@ -4683,12 +4419,12 @@ function Start-LoginMonitor {
|
||||
Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)"
|
||||
}
|
||||
}
|
||||
$lastRcmCheckTime = Get-RdpRcm1149PollCursor
|
||||
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastWinRmCheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10)
|
||||
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
|
||||
$monitorEvents = @(4624, 4625, 4634, 4647, 4648)
|
||||
$monitorEvents = @(4624, 4625, 4648)
|
||||
|
||||
$script:MonitorInMainLoop = $true
|
||||
while ($true) {
|
||||
@@ -4754,24 +4490,6 @@ function Start-LoginMonitor {
|
||||
if ($event.Id -eq 4648) {
|
||||
$shouldIgnore = $true
|
||||
$ignoreReason = 'EventID 4648 excluded (MonitorInteractiveOnly)'
|
||||
} elseif ($event.Id -in 4634, 4647) {
|
||||
if (-not $osKind.IsWorkstation) {
|
||||
$shouldIgnore = $true
|
||||
$ignoreReason = 'logoff 4634/4647 only on workstations (server LT3 is Kerberos/LDAP noise)'
|
||||
} else {
|
||||
$interactiveTypes = @(10)
|
||||
$modeLabel = 'workstation logoff LT10'
|
||||
if ($interactiveTypes -notcontains $eventInfo.LogonType) {
|
||||
$allow4647Workstation = (
|
||||
$event.Id -eq 4647 -and
|
||||
$eventInfo.LogonType -eq 0
|
||||
)
|
||||
if (-not $allow4647Workstation) {
|
||||
$shouldIgnore = $true
|
||||
$ignoreReason = "LogonType $($eventInfo.LogonType) not in $modeLabel"
|
||||
}
|
||||
}
|
||||
}
|
||||
} elseif ($event.Id -in 4624, 4625) {
|
||||
if ($osKind.IsWorkstation) {
|
||||
$interactiveTypes = @(10)
|
||||
@@ -4823,46 +4541,6 @@ function Start-LoginMonitor {
|
||||
}
|
||||
}
|
||||
|
||||
if ($event.Id -in 4634, 4647) {
|
||||
$dedupKeyLogoff = Get-RdpLogoffNotifyDedupKey -SecurityLogComputerName $event.MachineName `
|
||||
-Username $eventInfo.Username -LogonType $eventInfo.LogonType
|
||||
if (-not (Test-RdpLogoffNotifyDedupAllow -DedupKey $dedupKeyLogoff)) {
|
||||
Write-Log "Notify dedup logoff $($event.Id): User=$($eventInfo.Username) LT=$($eventInfo.LogonType) (window ${LoginSuccessNotifyDedupSeconds}s)"
|
||||
continue
|
||||
}
|
||||
|
||||
$formattedMessage = Format-LoginEvent -EventID $event.Id `
|
||||
-Username $eventInfo.Username `
|
||||
-ComputerName $eventInfo.ComputerName `
|
||||
-SourceIP $eventInfo.SourceIP `
|
||||
-ProcessName $eventInfo.ProcessName `
|
||||
-TimeCreated $eventInfo.TimeCreated `
|
||||
-LogonType $eventInfo.LogonType `
|
||||
-LogonTypeName $logonTypeName `
|
||||
-SecurityLogComputerName $event.MachineName
|
||||
|
||||
$sacDetails = @{
|
||||
user = $eventInfo.Username
|
||||
ip_address = $eventInfo.SourceIP
|
||||
logon_type = $eventInfo.LogonType
|
||||
event_id_windows = [int]$event.Id
|
||||
workstation_name = $eventInfo.ComputerName
|
||||
}
|
||||
if ($null -ne $eventInfo.SessionId -and -not [string]::IsNullOrWhiteSpace([string]$eventInfo.SessionId)) {
|
||||
$sacDetails['session_id'] = [string]$eventInfo.SessionId
|
||||
}
|
||||
|
||||
Write-Log "Notify logoff: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
|
||||
Send-MonitorNotification -Message $formattedMessage `
|
||||
-EmailSubject "RDP Login Monitor: выход (ID $($event.Id))" `
|
||||
-SacEventType 'rdp.session.logoff' -SacSeverity 'info' `
|
||||
-SacTitle "RDP session logoff $($event.Id)" `
|
||||
-SacSummary "Logoff $($event.Id) $($eventInfo.Username)" `
|
||||
-SacOccurredAt $eventInfo.TimeCreated `
|
||||
-SacDetails $sacDetails | Out-Null
|
||||
continue
|
||||
}
|
||||
|
||||
if ($event.Id -eq 4625 -and $FailedLogonRateLimitEnabled) {
|
||||
$rl = Get-FailedLogonRateLimitDecision4625 -SourceIP $eventInfo.SourceIP `
|
||||
-ComputerName $eventInfo.ComputerName -Username $eventInfo.Username `
|
||||
@@ -4966,14 +4644,6 @@ function Start-LoginMonitor {
|
||||
Write-Log "Skip RDG 303: ephemeral channel (SessionDuration=0, User=$($ei.Username), Target=$($ei.InternalIP), ErrorCode=$($ei.ErrorCode))"
|
||||
continue
|
||||
}
|
||||
if ($event.Id -eq 302) {
|
||||
$dedupKey302 = Get-Rdg302NotifyDedupKey -Username $ei.Username `
|
||||
-InternalIP $ei.InternalIP -ExternalIP $ei.ExternalIP
|
||||
if (Test-RdpMonitorNotifyDedup -Key $dedupKey302 -WindowSeconds $Rdg302NotifyDedupSeconds) {
|
||||
Write-Log "Notify dedup RDG 302: User=$($ei.Username) Target=$($ei.InternalIP) Ext=$($ei.ExternalIP) (window ${Rdg302NotifyDedupSeconds}s)"
|
||||
continue
|
||||
}
|
||||
}
|
||||
$msg = Format-RDGatewayEvent -EventID $event.Id `
|
||||
-Username $ei.Username `
|
||||
-ExternalIP $ei.ExternalIP `
|
||||
@@ -5020,41 +4690,12 @@ function Start-LoginMonitor {
|
||||
} -ErrorAction SilentlyContinue
|
||||
|
||||
if ($rcmEvents) {
|
||||
$rcmNotified = 0
|
||||
$rcmSkipped = 0
|
||||
foreach ($event in $rcmEvents) {
|
||||
if ($event.TimeCreated -le $lastRcmCheckTime) { continue }
|
||||
$rcmInfo = Get-Rcm1149EventInfo -Event $event
|
||||
if ($rcmInfo.Username -eq '-' -or [string]::IsNullOrWhiteSpace($rcmInfo.Username)) {
|
||||
$fromQw = Get-Rcm1149UsernameFromQwinsta
|
||||
if (-not [string]::IsNullOrWhiteSpace($fromQw)) {
|
||||
Write-Log "RCM 1149: EventLog user empty — qwinsta fallback User=$fromQw"
|
||||
$rcmInfo.Username = $fromQw
|
||||
}
|
||||
}
|
||||
if ($rcmInfo.Username -like "*$") {
|
||||
$rcmSkipped++
|
||||
Write-Log "Skip 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) — machine account"
|
||||
continue
|
||||
}
|
||||
if ($rcmInfo.Username -like "*$") { continue }
|
||||
if (Should-IgnoreEvent -Username $rcmInfo.Username -ProcessName "-" `
|
||||
-ComputerName "-" -EventID 1149 -LogonType 10 -SourceIP $rcmInfo.ClientIP) {
|
||||
$rcmSkipped++
|
||||
$ud = Get-Rcm1149UserDataEventInfoMap -Event $event
|
||||
$p1 = if ($ud.ContainsKey('Param1')) { $ud['Param1'] } else { '' }
|
||||
$p2 = if ($ud.ContainsKey('Param2')) { $ud['Param2'] } else { '' }
|
||||
$p3 = if ($ud.ContainsKey('Param3')) { $ud['Param3'] } else { '' }
|
||||
Write-Log "Skip 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) — built-in exclusion or ignore.lst (EventXML Param1=$p1 Param2=$p2 Param3=$p3)"
|
||||
continue
|
||||
}
|
||||
|
||||
$dedupKey1149 = Get-RdpLoginSuccessNotifyDedupKey -SecurityLogComputerName $event.MachineName `
|
||||
-Username $rcmInfo.Username -SourceIP $rcmInfo.ClientIP -LogonType 10
|
||||
if (-not (Test-RdpLoginSuccessNotifyDedupAllow -DedupKey $dedupKey1149)) {
|
||||
$rcmSkipped++
|
||||
Write-Log "Notify dedup 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) (window ${LoginSuccessNotifyDedupSeconds}s)"
|
||||
continue
|
||||
}
|
||||
-ComputerName "-" -EventID 1149 -LogonType 10 -SourceIP $rcmInfo.ClientIP) { continue }
|
||||
|
||||
$msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP `
|
||||
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
||||
@@ -5070,13 +4711,8 @@ function Start-LoginMonitor {
|
||||
ip_address = $rcmInfo.ClientIP
|
||||
event_id_windows = 1149
|
||||
} | Out-Null
|
||||
$rcmNotified++
|
||||
}
|
||||
$lastRcmCheckTime = Update-MonitorPollCursor -CurrentCursor $lastRcmCheckTime -Events @($rcmEvents)
|
||||
Set-RdpRcm1149PollCursor -Cursor $lastRcmCheckTime
|
||||
if ($rcmNotified -gt 0 -or $rcmSkipped -gt 0) {
|
||||
Write-Log "RCM 1149 poll: fetched=$(@($rcmEvents).Count) notified=$rcmNotified skipped=$rcmSkipped cursor=$($lastRcmCheckTime.ToString('dd.MM.yyyy HH:mm:ss'))"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5299,7 +4935,7 @@ function Start-LoginMonitor {
|
||||
$nextReportCheck = Check-AndSendDailyReport
|
||||
}
|
||||
if ($script:SacClientLoaded) {
|
||||
Invoke-SacFlushSpool | Out-Null
|
||||
Invoke-SacFlushSpool -MaxFiles 5 | Out-Null
|
||||
if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) {
|
||||
Invoke-SacProcessPendingCommands | Out-Null
|
||||
$script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# RDP Login Monitor
|
||||
|
||||
**Версия:** `2.1.14-SAC` (`$ScriptVersion` + `version.txt`)
|
||||
**Версия:** `2.1.2-SAC` (`$ScriptVersion` + `version.txt`)
|
||||
|
||||
PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты.
|
||||
|
||||
@@ -20,21 +20,20 @@ powershell -NoProfile -ExecutionPolicy Bypass -File "\\<DC>\NETLOGON\RDP-login-m
|
||||
2. `Deploy-LoginMonitor.ps1 -SourceShareRoot` (тот же алгоритм, что при GPO)
|
||||
3. Нужны: WinRM, domain admin в SAC, доступ ПК к `SAC_PUBLIC_URL`, `rdp_git_repo_url` в **Настройки → Обновления агентов**
|
||||
|
||||
Подробнее: [security-alert-center — agent-control-plane](https://git.papatramp.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-control-plane.md) §4.3.
|
||||
Подробнее: [security-alert-center — agent-control-plane](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-control-plane.md) §4.3.
|
||||
|
||||
## События в SAC
|
||||
|
||||
| Источник | Тип SAC |
|
||||
|----------|---------|
|
||||
| Security 4624/4625 | `rdp.login.*` |
|
||||
| Security 4634/4647 (прямой RDP, **только рабочая станция**, LT10) | `rdp.session.logoff` → закрытие сессии в SAC |
|
||||
| Security 5140 | `smb.admin_share.access` |
|
||||
| WinRM Operational 91 | `winrm.session.started` |
|
||||
| RD Gateway 302/303 | `rdg.connection.*` → flap 302→303 в SAC |
|
||||
| Security 4740 (КД) | `auth.account.locked` |
|
||||
| Агент | `agent.heartbeat`, `report.daily.rdp`, `agent.inventory` |
|
||||
|
||||
**SAC:** `Sac-Client.ps1`, `$UseSAC` — [agent-integration.md](https://git.papatramp.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-integration.md). Poll команд `qwinsta`/`logoff` (≥ 2.1.0-SAC).
|
||||
**SAC:** `Sac-Client.ps1`, `$UseSAC` — [agent-integration.md](https://git.kalinamall.ru/PapaTramp/security-alert-center/src/branch/main/docs/agent-integration.md). Poll команд `qwinsta`/`logoff` (≥ 2.1.0-SAC).
|
||||
|
||||
## Ключевые файлы
|
||||
|
||||
@@ -55,7 +54,7 @@ Login_Monitor.ps1 -CheckSac
|
||||
## Документация
|
||||
|
||||
- [Docs/README.md](Docs/README.md) — развёртывание, Exchange
|
||||
- [security-alert-center](https://git.papatramp.ru/PapaTramp/security-alert-center) — сервер, RDG flap, qwinsta через SAC UI/Seaca
|
||||
- [security-alert-center](https://git.kalinamall.ru/PapaTramp/security-alert-center) — сервер, RDG flap, qwinsta через SAC UI/Seaca
|
||||
|
||||
## English
|
||||
|
||||
|
||||
+1
-3
@@ -1,7 +1,5 @@
|
||||
# RDP Login Monitor
|
||||
|
||||
**Version:** `2.1.14-SAC` (`$ScriptVersion` + `version.txt`)
|
||||
|
||||
PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) notifications.
|
||||
|
||||
## Recommended layout
|
||||
@@ -92,7 +90,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 is built into **`Login_Monitor.ps1`** (`-Watchdog`); scheduled tasks **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`** are registered by **`-InstallTasks`**.
|
||||
- **`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`**.
|
||||
- **`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.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Запрос задач планировщика RDP-login-monitor через schtasks /Query /XML (fallback для Get-ScheduledTask).
|
||||
#>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Graceful restart RDP Login Monitor без Stop-Process.
|
||||
.DESCRIPTION
|
||||
|
||||
+17
-177
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Клиент Security Alert Center для RDP-login-monitor.
|
||||
.DESCRIPTION
|
||||
@@ -420,7 +420,6 @@ 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
|
||||
}
|
||||
@@ -431,7 +430,7 @@ function Test-SacHealth {
|
||||
$base = Get-SacBaseUrl
|
||||
if ([string]::IsNullOrWhiteSpace($base)) { return $false }
|
||||
|
||||
$timeout = Get-SacTimeoutSecResolved
|
||||
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||
try {
|
||||
Invoke-SacTlsPrep
|
||||
$resp = Invoke-WebRequest -Uri "$base/health" -Method Get -UseBasicParsing -TimeoutSec $timeout
|
||||
@@ -475,84 +474,6 @@ function Move-SacSpoolToRejected {
|
||||
Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Test-SacGuidString {
|
||||
param([string]$Value)
|
||||
if ([string]::IsNullOrWhiteSpace($Value)) { return $false }
|
||||
return ($Value.Trim() -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$')
|
||||
}
|
||||
|
||||
function Get-SacEventIdFromSpoolFileName {
|
||||
param([string]$Path)
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) { return $null }
|
||||
$base = [System.IO.Path]::GetFileNameWithoutExtension($Path)
|
||||
if (Test-SacGuidString -Value $base) {
|
||||
return $base.ToLowerInvariant()
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SacEventIdFromJsonText {
|
||||
param([string]$JsonText)
|
||||
if ([string]::IsNullOrWhiteSpace($JsonText)) { return $null }
|
||||
if ($JsonText -match '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') {
|
||||
return $Matches[1].ToLowerInvariant()
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Test-SacSpoolBytesCorrupt {
|
||||
param([byte[]]$Bytes)
|
||||
if ($null -eq $Bytes -or $Bytes.Length -lt 2) { return $true }
|
||||
# UTF-16/UTF-32 с нулевым стартом или «пустой» spool — не JSON (см. hex 00 00 00 00).
|
||||
if ($Bytes[0] -eq 0 -and $Bytes[1] -eq 0) { return $true }
|
||||
return $false
|
||||
}
|
||||
|
||||
function Read-SacSpoolFileBytes {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
return [System.IO.File]::ReadAllBytes($Path)
|
||||
}
|
||||
|
||||
function Read-SacSpoolFileText {
|
||||
param([Parameter(Mandatory = $true)][string]$Path)
|
||||
$bytes = Read-SacSpoolFileBytes -Path $Path
|
||||
if (Test-SacSpoolBytesCorrupt -Bytes $bytes) { return $null }
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0xFF -and $bytes[1] -eq 0xFE) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes)
|
||||
}
|
||||
if ($bytes.Length -ge 2 -and $bytes[0] -eq 0x7B -and $Bytes[1] -eq 0) {
|
||||
return [System.Text.Encoding]::Unicode.GetString($bytes)
|
||||
}
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
return $utf8.GetString($bytes)
|
||||
}
|
||||
|
||||
function Move-SacSpoolFileToRejected {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$SpoolFilePath,
|
||||
[string]$Reason = ''
|
||||
)
|
||||
$eventId = Get-SacEventIdFromSpoolFileName -Path $SpoolFilePath
|
||||
if ($Reason) {
|
||||
$label = if ($eventId) { "$eventId.json" } else { [System.IO.Path]::GetFileName($SpoolFilePath) }
|
||||
Write-SacLog "WARN: SAC spool → rejected ($Reason): $label"
|
||||
}
|
||||
if ($eventId) {
|
||||
Move-SacSpoolToRejected -EventId $eventId
|
||||
return
|
||||
}
|
||||
$dir = Get-SacSpoolDirResolved
|
||||
$name = [System.IO.Path]::GetFileName($SpoolFilePath)
|
||||
$src = Join-Path $dir $name
|
||||
if (-not (Test-Path -LiteralPath $src)) { return }
|
||||
$rejDir = Join-Path $dir 'rejected'
|
||||
if (-not (Test-Path -LiteralPath $rejDir)) {
|
||||
New-Item -ItemType Directory -Path $rejDir -Force | Out-Null
|
||||
}
|
||||
$dst = Join-Path $rejDir $name
|
||||
Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
function Get-SacPostBodyBytes {
|
||||
param([string]$JsonText)
|
||||
$bytes = Get-SacUtf8Bytes -Text $JsonText
|
||||
@@ -667,10 +588,7 @@ function Write-SacPostBodyDiagnostic {
|
||||
}
|
||||
|
||||
function Invoke-SacPostPayload {
|
||||
param(
|
||||
[string]$JsonBody,
|
||||
[string]$SpoolFilePath = ''
|
||||
)
|
||||
param([string]$JsonBody)
|
||||
|
||||
if (-not (Test-SacConfigured)) { return $false }
|
||||
if (-not (Test-SacShouldAttemptSend)) { return $false }
|
||||
@@ -679,20 +597,16 @@ function Invoke-SacPostPayload {
|
||||
if ([string]::IsNullOrWhiteSpace($ingest)) { return $false }
|
||||
|
||||
$jsonText = Repair-SacJsonText -Text (Get-SacSingleString -Value $JsonBody -Label 'spool payload')
|
||||
$eventId = Get-SacEventIdFromJsonText -JsonText $jsonText
|
||||
if (-not $eventId) {
|
||||
if (-not [string]::IsNullOrWhiteSpace($SpoolFilePath)) {
|
||||
Move-SacSpoolFileToRejected -SpoolFilePath $SpoolFilePath -Reason 'no event_id in JSON body'
|
||||
} else {
|
||||
Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST'
|
||||
}
|
||||
if ($jsonText -notmatch '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') {
|
||||
Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST'
|
||||
return $false
|
||||
}
|
||||
$eventId = $Matches[1]
|
||||
$eventType = 'unknown'
|
||||
if ($jsonText -match '"type"\s*:\s*"([^"]+)"') {
|
||||
$eventType = $Matches[1]
|
||||
}
|
||||
$timeout = Get-SacTimeoutSecResolved
|
||||
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||
$spoolOnFailure = $true
|
||||
$bodyBytes = $null
|
||||
|
||||
@@ -812,31 +726,6 @@ function Test-SacHeartbeatOnlyEventType {
|
||||
return ($EventType -eq 'agent.heartbeat')
|
||||
}
|
||||
|
||||
function Test-SacDailyReportEventType {
|
||||
param([string]$EventType)
|
||||
return ($EventType -in @('report.daily.rdp', 'report.daily.ssh'))
|
||||
}
|
||||
|
||||
function Get-SacTimeoutSecResolved {
|
||||
if ($SacTimeoutSec) {
|
||||
$n = 0
|
||||
if ([int]::TryParse([string]$SacTimeoutSec, [ref]$n) -and $n -gt 0) {
|
||||
return $n
|
||||
}
|
||||
}
|
||||
return 45
|
||||
}
|
||||
|
||||
function Get-SacSpoolFlushMaxFilesResolved {
|
||||
if (Get-Variable -Name SacSpoolFlushMaxFiles -ErrorAction SilentlyContinue) {
|
||||
$n = 0
|
||||
if ([int]::TryParse([string]$SacSpoolFlushMaxFiles, [ref]$n) -and $n -gt 0) {
|
||||
return $n
|
||||
}
|
||||
}
|
||||
return 50
|
||||
}
|
||||
|
||||
function Merge-SacNotifyDetails {
|
||||
param(
|
||||
[hashtable]$Details = $null,
|
||||
@@ -909,20 +798,6 @@ function Send-NotifyOrSac {
|
||||
return (Send-SacEvent @sacEventArgs)
|
||||
}
|
||||
|
||||
# Суточный отчёт — только SAC/spool; при сбое ingest не дублировать в локальный Telegram.
|
||||
if (Test-SacDailyReportEventType -EventType $EventType) {
|
||||
if ($mode -eq 'off') {
|
||||
return $false
|
||||
}
|
||||
$merged = Merge-SacNotifyDetails -Details $Details -TelegramVia 'sac'
|
||||
$sacEventArgs = Get-SacEventInvokeArgs -EventType $EventType -Severity $Severity -Title $Title -Summary $Summary -Details $merged -OccurredAt $OccurredAt
|
||||
if (Send-SacEvent @sacEventArgs) {
|
||||
return $true
|
||||
}
|
||||
Write-SacLog 'WARN: daily report не принят SAC — остаётся в spool (локальный Telegram пропущен)'
|
||||
return $false
|
||||
}
|
||||
|
||||
switch ($mode) {
|
||||
'off' {
|
||||
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
||||
@@ -954,11 +829,7 @@ function Send-NotifyOrSac {
|
||||
}
|
||||
|
||||
function Invoke-SacFlushSpool {
|
||||
param([int]$MaxFiles = 0)
|
||||
|
||||
if ($MaxFiles -le 0) {
|
||||
$MaxFiles = Get-SacSpoolFlushMaxFilesResolved
|
||||
}
|
||||
param([int]$MaxFiles = 20)
|
||||
|
||||
$mode = Get-SacNormalizedMode
|
||||
if ($mode -eq 'off') { return }
|
||||
@@ -967,46 +838,15 @@ function Invoke-SacFlushSpool {
|
||||
$dir = Get-SacSpoolDirResolved
|
||||
if (-not (Test-Path -LiteralPath $dir)) { return }
|
||||
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue)
|
||||
$daily = @()
|
||||
$other = @()
|
||||
foreach ($f in $files) {
|
||||
$eventType = ''
|
||||
try {
|
||||
$raw = Read-SacSpoolFileText -Path $f.FullName
|
||||
if (-not [string]::IsNullOrWhiteSpace($raw)) {
|
||||
$obj = $raw | ConvertFrom-Json -ErrorAction Stop
|
||||
if ($null -ne $obj.type) { $eventType = [string]$obj.type }
|
||||
}
|
||||
} catch {
|
||||
$eventType = ''
|
||||
}
|
||||
if (Test-SacDailyReportEventType -EventType $eventType) {
|
||||
$daily += $f
|
||||
} else {
|
||||
$other += $f
|
||||
}
|
||||
}
|
||||
$ordered = @(
|
||||
@($daily | Sort-Object LastWriteTime)
|
||||
@($other | Sort-Object LastWriteTime)
|
||||
)
|
||||
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime)
|
||||
$count = 0
|
||||
foreach ($f in $ordered) {
|
||||
foreach ($f in $files) {
|
||||
$count++
|
||||
if ($count -gt $MaxFiles) { break }
|
||||
try {
|
||||
$bytes = Read-SacSpoolFileBytes -Path $f.FullName
|
||||
if (Test-SacSpoolBytesCorrupt -Bytes $bytes) {
|
||||
Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'null or empty payload'
|
||||
continue
|
||||
}
|
||||
$json = Read-SacSpoolFileText -Path $f.FullName
|
||||
if ([string]::IsNullOrWhiteSpace($json)) {
|
||||
Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'unreadable payload'
|
||||
continue
|
||||
}
|
||||
Invoke-SacPostPayload -JsonBody $json -SpoolFilePath $f.FullName | Out-Null
|
||||
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||
$json = [System.IO.File]::ReadAllText($f.FullName, $utf8)
|
||||
Invoke-SacPostPayload -JsonBody $json | Out-Null
|
||||
} catch {
|
||||
Write-SacLog "WARN: SAC spool flush failed for $($f.Name): $($_.Exception.Message)"
|
||||
}
|
||||
@@ -1029,7 +869,7 @@ function Get-SacAgentCommandResultUrl {
|
||||
function Invoke-SacHttpGet {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[int]$TimeoutSec = 45
|
||||
[int]$TimeoutSec = 12
|
||||
)
|
||||
Invoke-SacTlsPrep
|
||||
try {
|
||||
@@ -1055,7 +895,7 @@ function Invoke-SacHttpPostJson {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$Uri,
|
||||
[Parameter(Mandatory = $true)][string]$JsonBody,
|
||||
[int]$TimeoutSec = 45
|
||||
[int]$TimeoutSec = 12
|
||||
)
|
||||
Invoke-SacTlsPrep
|
||||
$bytes = Get-SacUtf8Bytes -Text $JsonBody
|
||||
@@ -1184,7 +1024,7 @@ function Submit-SacAgentCommandResult {
|
||||
stdout = $Stdout
|
||||
stderr = $Stderr
|
||||
} | ConvertTo-Json -Compress
|
||||
$timeout = Get-SacTimeoutSecResolved
|
||||
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||
$resp = Invoke-SacHttpPostJson -Uri $url -JsonBody $body -TimeoutSec $timeout
|
||||
return ($resp.StatusCode -in 200, 201)
|
||||
}
|
||||
@@ -1197,7 +1037,7 @@ function Invoke-SacProcessPendingCommands {
|
||||
if ([string]::IsNullOrWhiteSpace($pollUrl)) { return 0 }
|
||||
|
||||
$agentId = Get-SacAgentInstanceId
|
||||
$timeout = Get-SacTimeoutSecResolved
|
||||
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||
$uri = "${pollUrl}?agent_instance_id=$([uri]::EscapeDataString($agentId))"
|
||||
$get = Invoke-SacHttpGet -Uri $uri -TimeoutSec $timeout
|
||||
if ($get.StatusCode -ne 200) {
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
<#
|
||||
.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 свежий."
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Пример локальных настроек Exchange-MailSecurity.ps1
|
||||
.DESCRIPTION
|
||||
@@ -29,11 +29,11 @@ $QueueMessageCountThreshold = 150
|
||||
# --- Пилот VIP (рекомендуется для первого запуска) ---
|
||||
# $VipMailboxesOnly = $true
|
||||
# $VipMailboxes = @(
|
||||
# 'director@kalinamall.ru',
|
||||
# 'cfo@kalinamall.ru'
|
||||
# 'director@example.com',
|
||||
# 'cfo@example.com'
|
||||
# )
|
||||
# $VipMailboxPatterns = @(
|
||||
# '*@kalinamall.ru' # опционально: все ящики домена из Get-Mailbox
|
||||
# '*@example.com' # опционально: все ящики домена из Get-Mailbox
|
||||
# )
|
||||
|
||||
# Первый ночной скан: не слать сотни алертов по уже существующим пересылкам
|
||||
@@ -46,9 +46,9 @@ $QueueMessageCountThreshold = 150
|
||||
# $SendInboxScanSummary = $true
|
||||
|
||||
# Удалённый EMS (если скрипт не на Exchange)
|
||||
# $ExchangeServerFqdn = 'fifth.kalinamall.ru'
|
||||
# $ExchangeServerFqdn = 'mail.example.com'
|
||||
|
||||
# Не сканировать Inbox rules (битое хранилище правил / Watson на Get-InboxRule)
|
||||
# $SkipInboxScanMailboxes = @(
|
||||
# 'k.selezneva@kalinamall.ru'
|
||||
# 'k.selezneva@example.com'
|
||||
# )
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Локальные настройки Login_Monitor.ps1
|
||||
.DESCRIPTION
|
||||
@@ -9,8 +9,8 @@
|
||||
#>
|
||||
|
||||
# --- Telegram (или DPAPI Base64 через Encrypt-DpapiForRdpMonitor.ps1) ---
|
||||
$TelegramBotToken = '8239219522:AAEyOZX3cwNfgGOMDkf-mgjTIuoaOh5gF7I'
|
||||
$TelegramChatID = '2843230'
|
||||
$TelegramBotToken = 'YOUR_BOT_TOKEN'
|
||||
$TelegramChatID = 'YOUR_CHAT_ID'
|
||||
# $TelegramBotTokenProtectedB64 = ''
|
||||
# $TelegramChatIDProtectedB64 = ''
|
||||
|
||||
@@ -29,17 +29,15 @@ $NotifyOrder = 'tg'
|
||||
# --- Подпись сервера в Telegram и SAC (host.display_name); пусто = $env:COMPUTERNAME ---
|
||||
# $ServerDisplayName = 'UNMS Kalina'
|
||||
# --- Явный IPv4 хоста для SAC (опционально; иначе автоопределение) ---
|
||||
# $ServerIPv4 = '192.168.160.57'
|
||||
# $ServerIPv4 = '10.0.0.10'
|
||||
|
||||
# --- Security Alert Center (SAC) ---
|
||||
# off | exclusive | dual | fallback — см. security-alert-center/docs/agent-integration.md
|
||||
$UseSAC = 'fallback'
|
||||
$SacUrl = 'https://sac.kalinamall.ru'
|
||||
$SacApiKey = 'sac_UkOsAT3UWiQS54KK5OJPBDCSucysQDrKFju28wmYiz8'
|
||||
$SacSpoolDir = 'C:\ProgramData\RDP-login-monitor\sac-spool'
|
||||
$SacTimeoutSec = 45
|
||||
$SacSpoolFlushMaxFiles = 50
|
||||
$SacSpoolMaxAgeHours = 72
|
||||
$SacUrl = 'https://sac.example.com'
|
||||
$SacApiKey = 'sac_CHANGE_ME'
|
||||
# $SacSpoolDir = 'C:\ProgramData\RDP-login-monitor\sac-spool'
|
||||
# $SacTimeoutSec = 12
|
||||
# $SacTlsSkipVerify = $false
|
||||
# $SacFallbackFailures = 5
|
||||
# $false = не слать report.daily.rdp с агента (суточный отчёт только из SAC)
|
||||
@@ -60,7 +58,6 @@ $StartupRebootDetectMinutes = 5
|
||||
$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)
|
||||
@@ -72,7 +69,7 @@ $GetInventory = $true
|
||||
|
||||
# --- Узкое исключение шумовых сетевых логонов (LogonType=3, Advapi) ---
|
||||
$IgnoreAdvapiNetworkLogonSourceIps = @(
|
||||
'192.168.160.57'
|
||||
'10.0.0.1'
|
||||
)
|
||||
# --- Exchange noise filter: 4624 + LogonType=3 + IP='-' (часто Outlook/почтовые клиенты) ---
|
||||
# Включайте на почтовом сервере, если нужен только полезный интерактивный сигнал.
|
||||
@@ -85,9 +82,9 @@ $MaxBackupDays = 31
|
||||
|
||||
# --- Блокировка учётной записи AD (4740) + IP из IIS ActiveSync ---
|
||||
# Мониторинг включается только на КД с именем $LockoutMonitorDomainController.
|
||||
$LockoutMonitorDomainController = 'K6A-DC3'
|
||||
$NetBiosDomainName = 'B26'
|
||||
$ExchangeIisLogPath = '\\fifth.kalinamall.ru\c$\inetpub\logs\LogFiles\W3SVC1'
|
||||
$LockoutMonitorDomainController = 'DC01'
|
||||
$NetBiosDomainName = 'CONTOSO'
|
||||
$ExchangeIisLogPath = '\\mail.example.com\c$\inetpub\logs\LogFiles\W3SVC1'
|
||||
$ExchangeServerHostForIisExclude = ''
|
||||
$ExchangeIisLogTailLines = 5000
|
||||
$ExchangeIisLogMinutesBeforeLockout = 30
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# 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)"
|
||||
@@ -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' { 'github' }
|
||||
'github' { 'origin' }
|
||||
'kalinamall' { 'kalinamall' }
|
||||
'papatramp' { 'papatramp' }
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Rewrite cross-repo URLs in tracked docs/config for the target Git host.
|
||||
# Rewrite cross-repo URLs in tracked docs/config for the target Git host.
|
||||
# Usage: .\scripts\Rewrite-GitHostUrls.ps1 github|kalinamall|papatramp
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
@@ -20,7 +20,7 @@ switch ($Target) {
|
||||
$BlobSuffix = '/src/branch/main'
|
||||
}
|
||||
'papatramp' {
|
||||
$Base = 'https://git.papatramp.ru/PapaTramp'
|
||||
$Base = 'https://git.papatramp.ru/PTah'
|
||||
$BlobSuffix = '/src/branch/main'
|
||||
}
|
||||
}
|
||||
@@ -28,20 +28,15 @@ switch ($Target) {
|
||||
$BaseHost = $Base -replace '^https://', ''
|
||||
|
||||
$patterns = @(
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/([^)/''"\s]+)/blob/main/'; To = "$Base/`${1}$BlobSuffix/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/([^)/''"\s]+)/blob/main/'; To = "$Base/`${1}$BlobSuffix/" }
|
||||
@{ From = 'https://git\.kalinamall\.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`${1}$BlobSuffix/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'https://git.papatramp.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'git.papatramp.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
@{ From = 'git.papatramp.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
@{ From = 'git.papatramp.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
@{ From = 'git.papatramp.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
@{ From = 'https://git.kalinamall.ru/PapaTramp/([^)/''"\s]+)/blob/main/'; To = "$Base/`$1$BlobSuffix/" }
|
||||
@{ From = 'https://git.kalinamall.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`$1$BlobSuffix/" }
|
||||
@{ From = 'https://git.kalinamall.ru/PapaTramp/([^)/''"\s]+)/src/branch/main/'; To = "$Base/`$1$BlobSuffix/" }
|
||||
@{ From = 'https://git.kalinamall.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'https://git.kalinamall.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'https://git.kalinamall.ru/PapaTramp/'; To = "$Base/" }
|
||||
@{ From = 'git.kalinamall.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
@{ From = 'git.kalinamall.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
@{ From = 'git.kalinamall.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||
)
|
||||
|
||||
$extensions = @('*.md', '*.json', '*.service', '*.example', '*.sh', '*.ps1', '*.yml', '*.yaml')
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
# 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://git.papatramp.ru/PapaTramp/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
|
||||
}
|
||||
|
||||
$mdFiles = @(git ls-files '*.md' 2>$null | Where-Object { $_ -and (Test-Path $_) })
|
||||
foreach ($rel in $mdFiles) {
|
||||
$path = Join-Path $Root $rel
|
||||
$md = Get-Content -LiteralPath $path -Raw
|
||||
$orig = $md
|
||||
$md = $md -replace 'https://git\.kalinamall\.ru/PapaTramp/([^)/\s]+)/src/branch/main/', 'https://git.papatramp.ru/PapaTramp/$1/src/branch/main/'
|
||||
$md = $md -replace 'https://git\.papatramp\.ru/PapaTramp/([^)/\s]+)/src/branch/main/', 'https://git.papatramp.ru/PapaTramp/$1/src/branch/main/'
|
||||
$md = $md -replace 'https://git\.kalinamall\.ru/PapaTramp/', 'https://git.papatramp.ru/PapaTramp/'
|
||||
$md = $md -replace 'https://git\.papatramp\.ru/PapaTramp/', 'https://git.papatramp.ru/PapaTramp/'
|
||||
if ($md -ne $orig) {
|
||||
Set-TrackedFileText -RelativePath $rel -Content $md
|
||||
}
|
||||
}
|
||||
|
||||
Write-Output 'Sanitize-ForGitHub: done'
|
||||
@@ -1,47 +0,0 @@
|
||||
# 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 }
|
||||
if ($file -like 'scripts/Push-Mirror.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)"
|
||||
+11
-25
@@ -1,10 +1,10 @@
|
||||
<#
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Obnovlyaet klon RDP-login-monitor s upstream git i kopiruet dist na NETLOGON.
|
||||
.DESCRIPTION
|
||||
Dlya servera publikatsii (napr. DC3). Po umolchaniyu GitHub (github.com/PTah).
|
||||
Na zakrytom zerkale ukazhite -GitUrl URL vashego Gitea.
|
||||
Posle fetch: vsegda reset --hard na kalinamall/main (bez merge), zatem clean -fd.
|
||||
Posle fetch: vsegda reset --hard na vetku upstream (bez merge), zatem clean -fd.
|
||||
Kopiruyutsya: polnyj spisok v Docs/deploy-netlogon-publish.md.
|
||||
.EXAMPLE
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1
|
||||
@@ -14,8 +14,8 @@
|
||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
||||
param(
|
||||
[string]$RepoPath = 'C:\Soft\Git\RDP-login-monitor',
|
||||
[string]$NetlogonDest = '\\b26\NETLOGON\RDP-login-monitor',
|
||||
[string]$GitUrl = 'https://git.papatramp.ru/PapaTramp/RDP-login-monitor.git',
|
||||
[string]$NetlogonDest = '\\dc.contoso.local\NETLOGON\RDP-login-monitor',
|
||||
[string]$GitUrl = 'https://github.com/PTah/RDP-login-monitor.git',
|
||||
[string]$GitBranch = 'main',
|
||||
[string]$LogFile = 'C:\soft\Logs\update-rdp-monitor.log',
|
||||
[switch]$SkipGitClean
|
||||
@@ -34,7 +34,6 @@ $DistFiles = @(
|
||||
'Install-DomainMonitors.ps1',
|
||||
'Deploy-DomainMonitors.ps1',
|
||||
'exchange_monitor.settings.example.ps1',
|
||||
'Diagnose-RdpLoginMonitor.ps1',
|
||||
'login_monitor.settings.example.ps1'
|
||||
)
|
||||
|
||||
@@ -113,38 +112,25 @@ function Ensure-GitRepository {
|
||||
function Get-ConfiguredGitRemoteName {
|
||||
$names = @(Invoke-GitCommand -Arguments @('remote') | ForEach-Object { "$_".Trim() } | Where-Object { $_ })
|
||||
if ($names.Count -eq 0) { return $null }
|
||||
if ('kalinamall' -in $names) { return 'kalinamall' }
|
||||
foreach ($n in $names) {
|
||||
$url = (& git -C $RepoPath remote get-url $n 2>$null)
|
||||
if ($url -match 'git\.kalinamall\.ru') { return $n }
|
||||
}
|
||||
if ('origin' -in $names) { return 'origin' }
|
||||
return $names[0]
|
||||
}
|
||||
|
||||
function Ensure-GitKalinamallRemote {
|
||||
function Ensure-GitUpstreamRemote {
|
||||
$name = Get-ConfiguredGitRemoteName
|
||||
if ($null -ne $name) {
|
||||
$url = (& git -C $RepoPath remote get-url $name 2>$null)
|
||||
if ($url -match 'git\.kalinamall\.ru') {
|
||||
Write-UpdateLog "Using remote: $name ($url)"
|
||||
return $name
|
||||
}
|
||||
Write-UpdateLog "Remote $name is not kalinamall ($url); adding kalinamall -> $GitUrl"
|
||||
} else {
|
||||
Write-UpdateLog "No remotes; adding kalinamall -> $GitUrl"
|
||||
Write-UpdateLog "Using remote: $name ($url)"
|
||||
return $name
|
||||
}
|
||||
if ('kalinamall' -in @(& git -C $RepoPath remote 2>$null)) {
|
||||
Invoke-GitCommand -Arguments @('remote', 'set-url', 'kalinamall', $GitUrl)
|
||||
return 'kalinamall'
|
||||
}
|
||||
Invoke-GitCommand -Arguments @('remote', 'add', 'kalinamall', $GitUrl)
|
||||
return 'kalinamall'
|
||||
Write-UpdateLog "No remotes; adding origin -> $GitUrl"
|
||||
Invoke-GitCommand -Arguments @('remote', 'add', 'origin', $GitUrl)
|
||||
return 'origin'
|
||||
}
|
||||
|
||||
function Update-Repository {
|
||||
Ensure-GitRepository
|
||||
$remote = Ensure-GitKalinamallRemote
|
||||
$remote = Ensure-GitUpstreamRemote
|
||||
Invoke-GitCommand -Arguments @('fetch', '--prune', $remote, $GitBranch)
|
||||
$upstream = "${remote}/${GitBranch}"
|
||||
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
2.1.15-SAC
|
||||
2.1.2-SAC
|
||||
|
||||
Reference in New Issue
Block a user