Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 599b152296 | |||
| c8dc681289 | |||
| c4c0556a2d | |||
| 2f44ddc29d | |||
| 6d160cef16 | |||
| 2e262ae878 | |||
| 6f08af7365 |
+23
-105
@@ -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 {
|
function Test-RdpMonitorSettingsNeedsSacBootstrap {
|
||||||
param([string]$SettingsPath)
|
param([string]$SettingsPath)
|
||||||
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
||||||
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
|
if ([string]::IsNullOrWhiteSpace($c)) { return $true }
|
||||||
|
|
||||||
if (Test-RdpMonitorSettingsHasPlaceholderSecrets -SettingsPath $SettingsPath) { return $true }
|
|
||||||
|
|
||||||
if ($c -notmatch '(?m)^\s*\$UseSAC\s*=') { return $true }
|
if ($c -notmatch '(?m)^\s*\$UseSAC\s*=') { return $true }
|
||||||
if ($c -match '(?m)^\s*\$UseSAC\s*=\s*[''"]off[''"]') {
|
if ($c -match '(?m)^\s*\$UseSAC\s*=\s*[''"]off[''"]') {
|
||||||
if ($c -notmatch '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_[^''"]+[''"]') { return $true }
|
if ($c -notmatch '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_[^''"]+[''"]') { return $true }
|
||||||
@@ -299,24 +285,12 @@ function Test-RdpMonitorSettingsNeedsServerDisplayNameHint {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-RdpMonitorSettingsDailyReportEnabledIsTrue {
|
function Test-RdpMonitorSettingsNeedsDailyReportHint {
|
||||||
param([string]$SettingsPath)
|
param([string]$SettingsPath)
|
||||||
if (-not (Test-Path -LiteralPath $SettingsPath)) { return $false }
|
if (-not (Test-Path -LiteralPath $SettingsPath)) { return $false }
|
||||||
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
$c = Get-RdpMonitorSettingsRaw -Path $SettingsPath
|
||||||
if ([string]::IsNullOrWhiteSpace($c)) { return $false }
|
if ([string]::IsNullOrWhiteSpace($c)) { return $false }
|
||||||
if ($c -match '(?m)^\s*\$DailyReportEnabled\s*=\s*\$(?:true)\b') { return $true }
|
if ($c -match '(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=') { return $false }
|
||||||
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 }
|
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -353,7 +327,7 @@ function Repair-RdpMonitorSettingsDailyReportAssignmentIfInvalid {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sync-RdpMonitorSettingsDailyReportEnabledToTrue {
|
function Update-RdpMonitorSettingsDailyReportHintIfMissing {
|
||||||
param([string]$LocalSettings)
|
param([string]$LocalSettings)
|
||||||
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
||||||
if (-not (Test-RdpMonitorSettingsNeedsDailyReportHint -SettingsPath $LocalSettings)) {
|
if (-not (Test-RdpMonitorSettingsNeedsDailyReportHint -SettingsPath $LocalSettings)) {
|
||||||
@@ -363,35 +337,24 @@ function Sync-RdpMonitorSettingsDailyReportEnabledToTrue {
|
|||||||
$c = Get-RdpMonitorSettingsRaw -Path $LocalSettings
|
$c = Get-RdpMonitorSettingsRaw -Path $LocalSettings
|
||||||
if ([string]::IsNullOrWhiteSpace($c)) { return $false }
|
if ([string]::IsNullOrWhiteSpace($c)) { return $false }
|
||||||
|
|
||||||
$dailyLine = '$DailyReportEnabled = $true # отчёт с агента (report.daily.rdp); $false или 0 — только SAC'
|
|
||||||
$hintBlock = @(
|
$hintBlock = @(
|
||||||
'# --- Суточный отчёт report.daily.rdp (агент или только SAC) ---'
|
'# --- Суточный отчёт report.daily.rdp (агент или только SAC) ---'
|
||||||
$dailyLine
|
'# $DailyReportEnabled = $false # по умолчанию: только SAC; $true или 1 — отчёт с агента'
|
||||||
'# Не пишите "= false" без $ — PowerShell воспримет false как команду.'
|
'# Не пишите "= false" без $ — PowerShell воспримет false как команду.'
|
||||||
) -join "`r`n"
|
) -join "`r`n"
|
||||||
|
|
||||||
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||||
Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force
|
Copy-Item -LiteralPath $LocalSettings -Destination $bak -Force
|
||||||
|
Write-DeployLog "Добавление закомментированного `$DailyReportEnabled в login_monitor.settings.ps1; резервная копия: $bak"
|
||||||
|
|
||||||
if ($c -match '(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=') {
|
$insertBefore = '(?m)^\s*#\s*---\s*Security Alert Center'
|
||||||
Write-DeployLog "login_monitor.settings.ps1: `$DailyReportEnabled → `$true (резервная копия: $bak)"
|
if ($c -match $insertBefore) {
|
||||||
$newContent = [regex]::Replace(
|
$newContent = [regex]::Replace($c, $insertBefore, ($hintBlock + "`r`n`r`n" + '$0'), 1)
|
||||||
$c,
|
|
||||||
'(?m)^\s*(\#\s*)?\$DailyReportEnabled\s*=.*$',
|
|
||||||
$dailyLine,
|
|
||||||
1
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
Write-DeployLog "login_monitor.settings.ps1: добавлен `$DailyReportEnabled = `$true (резервная копия: $bak)"
|
$newContent = ($c.TrimEnd() + "`r`n`r`n" + $hintBlock + "`r`n")
|
||||||
$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)
|
[System.IO.File]::WriteAllText($LocalSettings, $newContent.TrimEnd() + "`r`n", $Utf8Bom)
|
||||||
|
Write-DeployLog 'login_monitor.settings.ps1: добавлена подсказка # $DailyReportEnabled = $false'
|
||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +365,7 @@ function Sync-RdpMonitorSettingsDailyReportPatches {
|
|||||||
if (Repair-RdpMonitorSettingsDailyReportAssignmentIfInvalid -LocalSettings $LocalSettings) {
|
if (Repair-RdpMonitorSettingsDailyReportAssignmentIfInvalid -LocalSettings $LocalSettings) {
|
||||||
$changed = $true
|
$changed = $true
|
||||||
}
|
}
|
||||||
if (Sync-RdpMonitorSettingsDailyReportEnabledToTrue -LocalSettings $LocalSettings) {
|
if (Update-RdpMonitorSettingsDailyReportHintIfMissing -LocalSettings $LocalSettings) {
|
||||||
$changed = $true
|
$changed = $true
|
||||||
}
|
}
|
||||||
return $changed
|
return $changed
|
||||||
@@ -810,7 +773,6 @@ function Invoke-RdpMonitorSettingsPostPatches {
|
|||||||
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
if (-not (Test-Path -LiteralPath $LocalSettings)) { return $false }
|
||||||
$changed = $false
|
$changed = $false
|
||||||
if (Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
if (Update-RdpMonitorSettingsServerDisplayNameHintIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
||||||
if (Update-RdpMonitorSettingsServerIPv4HintIfMissing -LocalSettings $LocalSettings) { $changed = $true }
|
|
||||||
if (Sync-RdpMonitorSettingsDailyReportPatches -LocalSettings $LocalSettings) { $changed = $true }
|
if (Sync-RdpMonitorSettingsDailyReportPatches -LocalSettings $LocalSettings) { $changed = $true }
|
||||||
if (Sync-RdpMonitorSettingsExchangeNoisePatches -LocalSettings $LocalSettings) { $changed = $true }
|
if (Sync-RdpMonitorSettingsExchangeNoisePatches -LocalSettings $LocalSettings) { $changed = $true }
|
||||||
if (Repair-RdpMonitorSettingsWinRmLinesIfInvalid -LocalSettings $LocalSettings) { $changed = $true }
|
if (Repair-RdpMonitorSettingsWinRmLinesIfInvalid -LocalSettings $LocalSettings) { $changed = $true }
|
||||||
@@ -924,6 +886,8 @@ function Update-RdpMonitorSettingsServerDisplayNameHintIfMissing {
|
|||||||
$hintBlock = @(
|
$hintBlock = @(
|
||||||
'# --- Подпись сервера в Telegram и SAC (host.display_name); раскомментируйте при необходимости ---'
|
'# --- Подпись сервера в Telegram и SAC (host.display_name); раскомментируйте при необходимости ---'
|
||||||
"# `$ServerDisplayName = '$hostLabel'"
|
"# `$ServerDisplayName = '$hostLabel'"
|
||||||
|
'# --- Явный IPv4 хоста для SAC (опционально; иначе автоопределение) ---'
|
||||||
|
'# $ServerIPv4 = '''
|
||||||
) -join "`r`n"
|
) -join "`r`n"
|
||||||
|
|
||||||
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
$bak = "$LocalSettings.bak.$(Get-Date -Format 'yyyyMMdd_HHmmss')"
|
||||||
@@ -941,51 +905,6 @@ function Update-RdpMonitorSettingsServerDisplayNameHintIfMissing {
|
|||||||
return $true
|
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 {
|
function Get-RdpMonitorSacBlockFromExample {
|
||||||
param([string]$ExamplePath)
|
param([string]$ExamplePath)
|
||||||
if (-not (Test-Path -LiteralPath $ExamplePath)) { return $null }
|
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)') {
|
if ($ex -match '(?ms)(#\s*---\s*Security Alert Center.*?)(?=\r?\n#\s*---|\z)') {
|
||||||
return $Matches[1].TrimEnd()
|
return $Matches[1].TrimEnd()
|
||||||
}
|
}
|
||||||
Write-DeployLog "Предупреждение: в example нет блока Security Alert Center — patch SAC пропущен."
|
return @(
|
||||||
return $null
|
'# --- Security Alert Center (SAC) ---'
|
||||||
|
'$UseSAC = ''fallback'''
|
||||||
|
'$SacUrl = ''https://sac.example.com'''
|
||||||
|
'$SacApiKey = ''sac_CHANGE_ME'''
|
||||||
|
) -join "`r`n"
|
||||||
}
|
}
|
||||||
|
|
||||||
function Sync-RdpMonitorUseSacFallbackMode {
|
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 -match '(?m)^\s*\$UseSAC\s*=' -and $c -match '(?m)^\s*\$SacApiKey\s*=\s*[''"]sac_[^''"]+[''"]') {
|
||||||
if ($c -notmatch '(?m)^\s*\$UseSAC\s*=\s*[''"]off[''"]') {
|
if ($c -notmatch '(?m)^\s*\$UseSAC\s*=\s*[''"]off[''"]') {
|
||||||
if (-not (Test-RdpMonitorSettingsHasPlaceholderSecrets -SettingsPath $LocalSettings)) {
|
Write-DeployLog "login_monitor.settings.ps1: блок SAC уже задан, файл не меняем."
|
||||||
Write-DeployLog "login_monitor.settings.ps1: блок SAC уже задан, файл не меняем."
|
return $false
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1508,7 +1429,6 @@ try {
|
|||||||
|
|
||||||
$needsSettingsBootstrap = Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $settingsLocal
|
$needsSettingsBootstrap = Test-RdpMonitorSettingsNeedsSacBootstrap -SettingsPath $settingsLocal
|
||||||
$needsDisplayNameHint = Test-RdpMonitorSettingsNeedsServerDisplayNameHint -SettingsPath $settingsLocal
|
$needsDisplayNameHint = Test-RdpMonitorSettingsNeedsServerDisplayNameHint -SettingsPath $settingsLocal
|
||||||
$needsServerIPv4Hint = Test-RdpMonitorSettingsNeedsServerIPv4Hint -SettingsPath $settingsLocal
|
|
||||||
$needsDailyReportHint = Test-RdpMonitorSettingsNeedsDailyReportHint -SettingsPath $settingsLocal
|
$needsDailyReportHint = Test-RdpMonitorSettingsNeedsDailyReportHint -SettingsPath $settingsLocal
|
||||||
$needsDailyReportRepair = Test-RdpMonitorSettingsHasInvalidDailyReportAssignment -SettingsPath $settingsLocal
|
$needsDailyReportRepair = Test-RdpMonitorSettingsHasInvalidDailyReportAssignment -SettingsPath $settingsLocal
|
||||||
$needsExchangeNoisePatch = Test-RdpMonitorSettingsNeedsExchangeNoisePatch -SettingsPath $settingsLocal
|
$needsExchangeNoisePatch = Test-RdpMonitorSettingsNeedsExchangeNoisePatch -SettingsPath $settingsLocal
|
||||||
@@ -1517,7 +1437,7 @@ try {
|
|||||||
$needsStartupRebootDetectMinutes = Test-RdpMonitorSettingsNeedsStartupRebootDetectMinutes -SettingsPath $settingsLocal
|
$needsStartupRebootDetectMinutes = Test-RdpMonitorSettingsNeedsStartupRebootDetectMinutes -SettingsPath $settingsLocal
|
||||||
$needsBundleSync = Test-RdpMonitorDeployBundleNeedsSync -ShareRoot $shareRoot
|
$needsBundleSync = Test-RdpMonitorDeployBundleNeedsSync -ShareRoot $shareRoot
|
||||||
$needsTaskExecutionLimitFix = Test-RdpMonitorDeployMainTaskNeedsUnlimitedExecutionTime -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) {
|
if (Test-RdpMonitorExchangeServerRole) {
|
||||||
Write-DeployLog "Обнаружена роль Exchange — при необходимости допишем WinRM/4624 noise settings в login_monitor.settings.ps1."
|
Write-DeployLog "Обнаружена роль Exchange — при необходимости допишем WinRM/4624 noise settings в login_monitor.settings.ps1."
|
||||||
@@ -1546,10 +1466,8 @@ try {
|
|||||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нужна донастройка SAC в settings — продолжаем деплой."
|
Write-DeployLog "Версия совпадает ($shareVerRaw), но нужна донастройка SAC в settings — продолжаем деплой."
|
||||||
} elseif ($needsDisplayNameHint) {
|
} elseif ($needsDisplayNameHint) {
|
||||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет подсказки `$ServerDisplayName в settings — продолжаем деплой."
|
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет подсказки `$ServerDisplayName в settings — продолжаем деплой."
|
||||||
} elseif ($needsServerIPv4Hint) {
|
|
||||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет подсказки `$ServerIPv4 в settings — продолжаем деплой."
|
|
||||||
} elseif ($needsDailyReportHint) {
|
} elseif ($needsDailyReportHint) {
|
||||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но `$DailyReportEnabled не `$true — допишем/исправим и продолжим деплой."
|
Write-DeployLog "Версия совпадает ($shareVerRaw), но нет `$DailyReportEnabled в settings — продолжаем деплой."
|
||||||
} elseif ($needsDailyReportRepair) {
|
} elseif ($needsDailyReportRepair) {
|
||||||
Write-DeployLog "Версия совпадает ($shareVerRaw), но в settings некорректно задан `$DailyReportEnabled (= true/false без `$) — продолжаем деплой."
|
Write-DeployLog "Версия совпадает ($shareVerRaw), но в settings некорректно задан `$DailyReportEnabled (= true/false без `$) — продолжаем деплой."
|
||||||
} elseif ($needsExchangeNoisePatch) {
|
} elseif ($needsExchangeNoisePatch) {
|
||||||
|
|||||||
@@ -300,7 +300,6 @@ if (-not $isAdmin) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
$since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack))
|
$since = (Get-Date).AddMinutes(-1 * [math]::Abs($MinutesBack))
|
||||||
$RcmLogName = 'Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational'
|
|
||||||
$isWs = Test-IsWorkstationOs
|
$isWs = Test-IsWorkstationOs
|
||||||
[void]$report.AppendLine("OS ProductType workstation=$isWs (server mode LT 2/3/10, workstation LT 10)")
|
[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'))")
|
[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('')
|
||||||
[void]$report.AppendLine('UseSAC=exclusive: Telegram по rdp.login.success только из SAC (не локально агентом).')
|
[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.AppendLine('rdp.login.success = severity info; при SAC min_severity=warning Telegram не уйдёт, но событие в UI SAC должно быть.')
|
||||||
|
[void]$report.AppendLine('Проверьте SAC: type=rdp.login.success, hostname ITIS198, время входа, event_id из login_monitor.log.')
|
||||||
[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.')
|
|
||||||
|
|
||||||
$text = $report.ToString()
|
$text = $report.ToString()
|
||||||
[System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true))
|
[System.IO.File]::WriteAllText($OutputPath, $text, (New-Object System.Text.UTF8Encoding $true))
|
||||||
|
|||||||
@@ -2,37 +2,12 @@
|
|||||||
|
|
||||||
Скрипт **`update-rdp-monitor.ps1`** на сервере публикации (например DC3) выполняет `git pull` и копирует файлы в шару.
|
Скрипт **`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` |
|
| `$RepoPath` | `C:\Soft\Git\RDP-login-monitor` |
|
||||||
| `$NetlogonDest` | `\\dc.contoso.local\NETLOGON\RDP-login-monitor` *(заглушка — замените на свой DC)* |
|
| `$NetlogonDest` | `\\dc.contoso.local\NETLOGON\RDP-login-monitor` |
|
||||||
| `$GitUrl` | `https://github.com/PTah/RDP-login-monitor.git` |
|
| `$GitUrl` | `https://github.com/PTah/RDP-login-monitor.git` |
|
||||||
| `$GitBranch` | `main` |
|
| `$GitBranch` | `main` |
|
||||||
| `$LogFile` | `C:\soft\Logs\update-rdp-monitor.log` |
|
| `$LogFile` | `C:\soft\Logs\update-rdp-monitor.log` |
|
||||||
@@ -53,27 +28,9 @@ Test-Path '\\K6A-DC3\NETLOGON'
|
|||||||
|
|
||||||
## Запуск
|
## Запуск
|
||||||
|
|
||||||
**На сервере публикации** (клон репозитория + доступ к NETLOGON):
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Soft\Git\RDP-login-monitor\update-rdp-monitor.ps1 `
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1
|
||||||
-NetlogonDest '\\K6A-DC3\NETLOGON\RDP-login-monitor'
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\soft\update-rdp-monitor.ps1 -WhatIf
|
||||||
```
|
|
||||||
|
|
||||||
Другой 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/PTah/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
|
|
||||||
```
|
```
|
||||||
|
|
||||||
После pull обязательно проверьте **`version.txt`** на шаре — его номер определяет, подтянут ли обновления на клиентах. Метка может включать суффикс (например **`1.2.27-SAC`**); **`Deploy-LoginMonitor.ps1`** сравнивает её с **`deployed_version.txt`** по полной строке.
|
После 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.
|
**Требования:** domain admin в SAC; `SAC_PUBLIC_URL` доступен с ПК; `rdp_git_repo_url` в настройках обновлений. **`Deploy-LoginMonitor.ps1` на шаре NETLOGON для этого пути не обязателен** — скрипт приходит в zip.
|
||||||
|
|
||||||
Лог операции — в модалке SAC сразу при старте. См. [agent-control-plane.md](https://git.papatramp.ru/PTah/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.
|
||||||
|
|
||||||
## Версии
|
## Версии
|
||||||
|
|
||||||
|
|||||||
+17
-124
@@ -16,7 +16,7 @@
|
|||||||
- Добавлены исключения шума: DWM-*, UMFD-*, HealthMailbox*, Font Driver Host*, NtLmSsp и др.
|
- Добавлены исключения шума: DWM-*, UMFD-*, HealthMailbox*, Font Driver Host*, NtLmSsp и др.
|
||||||
- Heartbeat без дрейфа: используется nextHeartbeatTime (а не "прошло N секунд").
|
- Heartbeat без дрейфа: используется nextHeartbeatTime (а не "прошло N секунд").
|
||||||
- Win32_OperatingSystem.ProductType: 1 = рабочая станция (4624/4625 только LogonType 10 + при наличии журнала событие 1149 RCM);
|
- 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): на ЭТОЙ машине, под админом, выполните:
|
Секреты Telegram (DPAPI LocalMachine): на ЭТОЙ машине, под админом, выполните:
|
||||||
Add-Type -AssemblyName System.Security
|
Add-Type -AssemblyName System.Security
|
||||||
$p=[Text.Encoding]::UTF8.GetBytes('<токен>')
|
$p=[Text.Encoding]::UTF8.GetBytes('<токен>')
|
||||||
@@ -90,7 +90,7 @@ $script:SkipLogDetailLimit = 15
|
|||||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||||
$ScriptVersion = "2.1.9-SAC"
|
$ScriptVersion = "2.1.2-SAC"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -115,7 +115,6 @@ $LoginSuccessNotifyDedupSeconds = 90
|
|||||||
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
||||||
$GatewayPollCursorFile = Join-Path $script:InstallRoot "Logs\last_rdgateway_poll.txt"
|
$GatewayPollCursorFile = Join-Path $script:InstallRoot "Logs\last_rdgateway_poll.txt"
|
||||||
$SecurityPollCursorFile = Join-Path $script:InstallRoot "Logs\last_security_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"
|
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
|
||||||
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
||||||
$script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst"
|
$script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst"
|
||||||
@@ -135,17 +134,12 @@ $LastReportFile = Join-Path $script:InstallRoot "Logs\last_daily_report.txt"
|
|||||||
$EnableRDGatewayMonitoring = $true
|
$EnableRDGatewayMonitoring = $true
|
||||||
$RDGatewayLogName = "Microsoft-Windows-TerminalServices-Gateway/Operational"
|
$RDGatewayLogName = "Microsoft-Windows-TerminalServices-Gateway/Operational"
|
||||||
$RDGatewayEvents = @(302, 303)
|
$RDGatewayEvents = @(302, 303)
|
||||||
# Дубликаты RDG 302 (User+Target+ExternalIP) в окне N с — один alert в SAC/Telegram.
|
|
||||||
$Rdg302NotifyDedupSeconds = 90
|
|
||||||
# Макс. возраст сохранённого курсора RD Gateway (мин): если файл старше — replay не делаем.
|
# Макс. возраст сохранённого курсора RD Gateway (мин): если файл старше — replay не делаем.
|
||||||
$GatewayEventsLookbackMinutes = 60
|
$GatewayEventsLookbackMinutes = 60
|
||||||
# Security 4624/4625/4648: персистентный cursor + replay не старше N мин (медленный boot / поздний старт агента).
|
# Security 4624/4625/4648: персистентный cursor + replay не старше N мин (медленный boot / поздний старт агента).
|
||||||
$SecurityEventsLookbackMinutes = 60
|
$SecurityEventsLookbackMinutes = 60
|
||||||
# RCM 1149: персистентный cursor + replay (как Security).
|
|
||||||
$Rcm1149EventsLookbackMinutes = 60
|
|
||||||
|
|
||||||
# RDP Remote Connection Manager: User authentication succeeded — событие 1149 (workstation + RDS server)
|
# RDP Remote Connection Manager (workstations): User authentication succeeded — событие 1149
|
||||||
$EnableRcm1149Monitoring = 1
|
|
||||||
$RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational"
|
$RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational"
|
||||||
$RcmEventId = 1149
|
$RcmEventId = 1149
|
||||||
|
|
||||||
@@ -258,9 +252,7 @@ $SacUrl = ''
|
|||||||
$SacApiKey = ''
|
$SacApiKey = ''
|
||||||
$SacSpoolDir = ''
|
$SacSpoolDir = ''
|
||||||
$SacAgentIdFile = ''
|
$SacAgentIdFile = ''
|
||||||
$SacTimeoutSec = 45
|
$SacTimeoutSec = 12
|
||||||
$SacSpoolFlushMaxFiles = 50
|
|
||||||
$SacSpoolMaxAgeHours = 72
|
|
||||||
$SacTlsSkipVerify = $false
|
$SacTlsSkipVerify = $false
|
||||||
$SacFallbackFailures = 5
|
$SacFallbackFailures = 5
|
||||||
|
|
||||||
@@ -2645,7 +2637,7 @@ function Send-Heartbeat {
|
|||||||
if ($script:IsWorkstation) {
|
if ($script:IsWorkstation) {
|
||||||
$message += "`r`n📌 <b>Режим:</b> рабочая станция — Security 4624/4625 только LogonType 10 (RDP); при наличии журнала — также 1149 (Remote Connection Manager)."
|
$message += "`r`n📌 <b>Режим:</b> рабочая станция — Security 4624/4625 только LogonType 10 (RDP); при наличии журнала — также 1149 (Remote Connection Manager)."
|
||||||
} else {
|
} 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)
|
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
|
||||||
if ($ignoreEntries.Count -gt 0) {
|
if ($ignoreEntries.Count -gt 0) {
|
||||||
@@ -3235,10 +3227,8 @@ function Should-IgnoreEvent {
|
|||||||
if ($Username -like $p) { return $true }
|
if ($Username -like $p) { return $true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($EventID -ne 1149) {
|
if ($ComputerName -eq "Authz" -or $ComputerName -like "Authz*") { 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 }
|
||||||
if ($ComputerName -eq "NtLmSsp" -or $ComputerName -like "NtLmSsp*" -or $ComputerName -like "*NtLmSsp*") { return $true }
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($lp in $ExcludedLogonProcesses) {
|
foreach ($lp in $ExcludedLogonProcesses) {
|
||||||
if ($ProcessName -like "*$lp*") { return $true }
|
if ($ProcessName -like "*$lp*") { return $true }
|
||||||
@@ -3248,14 +3238,13 @@ function Should-IgnoreEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if ($SourceIP -eq "127.0.0.1" -or $SourceIP -like "fe80:*") { return $true }
|
if ($SourceIP -eq "127.0.0.1" -or $SourceIP -like "fe80:*") { return $true }
|
||||||
# RCM 1149 не содержит WorkstationName; caller передаёт ComputerName='-'.
|
if ($ComputerName -eq "-" -or $ComputerName -eq "N/A") { return $true }
|
||||||
if ($EventID -ne 1149 -and ($ComputerName -eq "-" -or $ComputerName -eq "N/A")) { return $true }
|
|
||||||
|
|
||||||
foreach ($pattern in $ExcludedComputerPatterns) {
|
foreach ($pattern in $ExcludedComputerPatterns) {
|
||||||
if ($ComputerName -like $pattern) { return $true }
|
if ($ComputerName -like $pattern) { return $true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($EventID -in 4624, 4625, 1149) {
|
if ($EventID -in 4624, 4625) {
|
||||||
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username `
|
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$EventID) -Username $Username `
|
||||||
-ComputerName $ComputerName -SourceIP $SourceIP) {
|
-ComputerName $ComputerName -SourceIP $SourceIP) {
|
||||||
return $true
|
return $true
|
||||||
@@ -3767,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 {
|
function Test-RdgGatewayBenignErrorCode {
|
||||||
param([string]$ErrorCode)
|
param([string]$ErrorCode)
|
||||||
if ([string]::IsNullOrWhiteSpace($ErrorCode)) { return $true }
|
if ([string]::IsNullOrWhiteSpace($ErrorCode)) { return $true }
|
||||||
@@ -3822,20 +3766,6 @@ function Test-RdgGatewayBenignErrorCode {
|
|||||||
return $false
|
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 {
|
function Get-RdgGatewaySacEventType {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][int]$EventId,
|
[Parameter(Mandatory = $true)][int]$EventId,
|
||||||
@@ -4398,7 +4328,7 @@ function Start-LoginMonitor {
|
|||||||
if ($osKind.IsWorkstation) {
|
if ($osKind.IsWorkstation) {
|
||||||
Write-Log "Режим рабочей станции: Security — только LogonType 10 (RDP); при наличии журнала — событие 1149 (Remote Connection Manager)."
|
Write-Log "Режим рабочей станции: Security — только LogonType 10 (RDP); при наличии журнала — событие 1149 (Remote Connection Manager)."
|
||||||
} else {
|
} else {
|
||||||
Write-Log "Режим сервера: Security — LogonType 2, 3, 10; при журнале RCM — 1149 (если `$EnableRcm1149Monitoring)."
|
Write-Log "Режим сервера: Security — LogonType 2, 3, 10"
|
||||||
}
|
}
|
||||||
if (Test-MonitorFeatureEnabled -Value $EnableAdminShareMonitoring) {
|
if (Test-MonitorFeatureEnabled -Value $EnableAdminShareMonitoring) {
|
||||||
Write-Log "Admin share: Security 5140 для $($AdminShareMonitorSuffixes -join ', ') (audit File Share; workstation + server)."
|
Write-Log "Admin share: Security 5140 для $($AdminShareMonitorSuffixes -join ', ') (audit File Share; workstation + server)."
|
||||||
@@ -4465,15 +4395,9 @@ function Start-LoginMonitor {
|
|||||||
$rdGatewayAvailable = $false
|
$rdGatewayAvailable = $false
|
||||||
if ($EnableRDGatewayMonitoring) { $rdGatewayAvailable = Test-RDGatewayLog }
|
if ($EnableRDGatewayMonitoring) { $rdGatewayAvailable = Test-RDGatewayLog }
|
||||||
|
|
||||||
$rcmMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableRcm1149Monitoring) -and (Test-RcmLogAvailable)
|
$rcmMonitoringEnabled = ($osKind.IsWorkstation -and (Test-RcmLogAvailable))
|
||||||
if (Test-MonitorFeatureEnabled -Value $EnableRcm1149Monitoring) {
|
if ($osKind.IsWorkstation -and -not $rcmMonitoringEnabled) {
|
||||||
if ($rcmMonitoringEnabled) {
|
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
|
||||||
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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$rcmShadowMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableRcmShadowControlMonitoring) -and (Test-RcmLogAvailable)
|
$rcmShadowMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableRcmShadowControlMonitoring) -and (Test-RcmLogAvailable)
|
||||||
@@ -4495,7 +4419,7 @@ function Start-LoginMonitor {
|
|||||||
Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)"
|
Write-Log "RD Gateway: журнал недоступен — опрос 302/303 отключён ($RDGatewayLogName)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
$lastRcmCheckTime = Get-RdpRcm1149PollCursor
|
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
$lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10)
|
$lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
$lastWinRmCheckTime = (Get-Date).AddSeconds(-10)
|
$lastWinRmCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
$lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10)
|
$lastAdminShare5140CheckTime = (Get-Date).AddSeconds(-10)
|
||||||
@@ -4720,14 +4644,6 @@ function Start-LoginMonitor {
|
|||||||
Write-Log "Skip RDG 303: ephemeral channel (SessionDuration=0, User=$($ei.Username), Target=$($ei.InternalIP), ErrorCode=$($ei.ErrorCode))"
|
Write-Log "Skip RDG 303: ephemeral channel (SessionDuration=0, User=$($ei.Username), Target=$($ei.InternalIP), ErrorCode=$($ei.ErrorCode))"
|
||||||
continue
|
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 `
|
$msg = Format-RDGatewayEvent -EventID $event.Id `
|
||||||
-Username $ei.Username `
|
-Username $ei.Username `
|
||||||
-ExternalIP $ei.ExternalIP `
|
-ExternalIP $ei.ExternalIP `
|
||||||
@@ -4774,30 +4690,12 @@ function Start-LoginMonitor {
|
|||||||
} -ErrorAction SilentlyContinue
|
} -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
if ($rcmEvents) {
|
if ($rcmEvents) {
|
||||||
$rcmNotified = 0
|
|
||||||
$rcmSkipped = 0
|
|
||||||
foreach ($event in $rcmEvents) {
|
foreach ($event in $rcmEvents) {
|
||||||
if ($event.TimeCreated -le $lastRcmCheckTime) { continue }
|
if ($event.TimeCreated -le $lastRcmCheckTime) { continue }
|
||||||
$rcmInfo = Get-Rcm1149EventInfo -Event $event
|
$rcmInfo = Get-Rcm1149EventInfo -Event $event
|
||||||
if ($rcmInfo.Username -like "*$") {
|
if ($rcmInfo.Username -like "*$") { continue }
|
||||||
$rcmSkipped++
|
|
||||||
Write-Log "Skip 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) — machine account"
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (Should-IgnoreEvent -Username $rcmInfo.Username -ProcessName "-" `
|
if (Should-IgnoreEvent -Username $rcmInfo.Username -ProcessName "-" `
|
||||||
-ComputerName "-" -EventID 1149 -LogonType 10 -SourceIP $rcmInfo.ClientIP) {
|
-ComputerName "-" -EventID 1149 -LogonType 10 -SourceIP $rcmInfo.ClientIP) { continue }
|
||||||
$rcmSkipped++
|
|
||||||
Write-Log "Skip 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP) — built-in exclusion or ignore.lst"
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
$msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP `
|
$msg = Format-Rcm1149Event -Username $rcmInfo.Username -ClientIP $rcmInfo.ClientIP `
|
||||||
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
||||||
@@ -4813,13 +4711,8 @@ function Start-LoginMonitor {
|
|||||||
ip_address = $rcmInfo.ClientIP
|
ip_address = $rcmInfo.ClientIP
|
||||||
event_id_windows = 1149
|
event_id_windows = 1149
|
||||||
} | Out-Null
|
} | Out-Null
|
||||||
$rcmNotified++
|
|
||||||
}
|
}
|
||||||
$lastRcmCheckTime = Update-MonitorPollCursor -CurrentCursor $lastRcmCheckTime -Events @($rcmEvents)
|
$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'))"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -5042,7 +4935,7 @@ function Start-LoginMonitor {
|
|||||||
$nextReportCheck = Check-AndSendDailyReport
|
$nextReportCheck = Check-AndSendDailyReport
|
||||||
}
|
}
|
||||||
if ($script:SacClientLoaded) {
|
if ($script:SacClientLoaded) {
|
||||||
Invoke-SacFlushSpool | Out-Null
|
Invoke-SacFlushSpool -MaxFiles 5 | Out-Null
|
||||||
if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) {
|
if ((Get-SacNormalizedMode) -ne 'off' -and $now -ge $script:NextSacCommandPollTime) {
|
||||||
Invoke-SacProcessPendingCommands | Out-Null
|
Invoke-SacProcessPendingCommands | Out-Null
|
||||||
$script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec)
|
$script:NextSacCommandPollTime = $now.AddSeconds($SacCommandPollIntervalSec)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# RDP Login Monitor
|
# RDP Login Monitor
|
||||||
|
|
||||||
**Версия:** `2.1.5-SAC` (`$ScriptVersion` + `version.txt`)
|
**Версия:** `2.1.2-SAC` (`$ScriptVersion` + `version.txt`)
|
||||||
|
|
||||||
PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты.
|
PowerShell-мониторинг Windows: RDP/RDS, RD Gateway, WinRM, admin share, блокировки УЗ, heartbeat, отчёты.
|
||||||
|
|
||||||
@@ -20,7 +20,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File "\\<DC>\NETLOGON\RDP-login-m
|
|||||||
2. `Deploy-LoginMonitor.ps1 -SourceShareRoot` (тот же алгоритм, что при GPO)
|
2. `Deploy-LoginMonitor.ps1 -SourceShareRoot` (тот же алгоритм, что при GPO)
|
||||||
3. Нужны: WinRM, domain admin в SAC, доступ ПК к `SAC_PUBLIC_URL`, `rdp_git_repo_url` в **Настройки → Обновления агентов**
|
3. Нужны: WinRM, domain admin в SAC, доступ ПК к `SAC_PUBLIC_URL`, `rdp_git_repo_url` в **Настройки → Обновления агентов**
|
||||||
|
|
||||||
Подробнее: [security-alert-center — agent-control-plane](https://git.papatramp.ru/PTah/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
|
||||||
|
|
||||||
@@ -33,7 +33,7 @@ powershell -NoProfile -ExecutionPolicy Bypass -File "\\<DC>\NETLOGON\RDP-login-m
|
|||||||
| Security 4740 (КД) | `auth.account.locked` |
|
| Security 4740 (КД) | `auth.account.locked` |
|
||||||
| Агент | `agent.heartbeat`, `report.daily.rdp`, `agent.inventory` |
|
| Агент | `agent.heartbeat`, `report.daily.rdp`, `agent.inventory` |
|
||||||
|
|
||||||
**SAC:** `Sac-Client.ps1`, `$UseSAC` — [agent-integration.md](https://git.papatramp.ru/PTah/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).
|
||||||
|
|
||||||
## Ключевые файлы
|
## Ключевые файлы
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ Login_Monitor.ps1 -CheckSac
|
|||||||
## Документация
|
## Документация
|
||||||
|
|
||||||
- [Docs/README.md](Docs/README.md) — развёртывание, Exchange
|
- [Docs/README.md](Docs/README.md) — развёртывание, Exchange
|
||||||
- [security-alert-center](https://git.papatramp.ru/PTah/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
|
## English
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# RDP Login Monitor
|
# RDP Login Monitor
|
||||||
|
|
||||||
**Version:** `2.1.5-SAC` (`$ScriptVersion` + `version.txt`)
|
|
||||||
|
|
||||||
PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) notifications.
|
PowerShell toolkit for monitoring Windows logons with Telegram and/or Email (SMTP) notifications.
|
||||||
|
|
||||||
## Recommended layout
|
## Recommended layout
|
||||||
|
|||||||
+16
-175
@@ -430,7 +430,7 @@ function Test-SacHealth {
|
|||||||
$base = Get-SacBaseUrl
|
$base = Get-SacBaseUrl
|
||||||
if ([string]::IsNullOrWhiteSpace($base)) { return $false }
|
if ([string]::IsNullOrWhiteSpace($base)) { return $false }
|
||||||
|
|
||||||
$timeout = Get-SacTimeoutSecResolved
|
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||||
try {
|
try {
|
||||||
Invoke-SacTlsPrep
|
Invoke-SacTlsPrep
|
||||||
$resp = Invoke-WebRequest -Uri "$base/health" -Method Get -UseBasicParsing -TimeoutSec $timeout
|
$resp = Invoke-WebRequest -Uri "$base/health" -Method Get -UseBasicParsing -TimeoutSec $timeout
|
||||||
@@ -474,84 +474,6 @@ function Move-SacSpoolToRejected {
|
|||||||
Move-Item -LiteralPath $src -Destination $dst -Force -ErrorAction SilentlyContinue
|
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 {
|
function Get-SacPostBodyBytes {
|
||||||
param([string]$JsonText)
|
param([string]$JsonText)
|
||||||
$bytes = Get-SacUtf8Bytes -Text $JsonText
|
$bytes = Get-SacUtf8Bytes -Text $JsonText
|
||||||
@@ -666,10 +588,7 @@ function Write-SacPostBodyDiagnostic {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-SacPostPayload {
|
function Invoke-SacPostPayload {
|
||||||
param(
|
param([string]$JsonBody)
|
||||||
[string]$JsonBody,
|
|
||||||
[string]$SpoolFilePath = ''
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not (Test-SacConfigured)) { return $false }
|
if (-not (Test-SacConfigured)) { return $false }
|
||||||
if (-not (Test-SacShouldAttemptSend)) { return $false }
|
if (-not (Test-SacShouldAttemptSend)) { return $false }
|
||||||
@@ -678,20 +597,16 @@ function Invoke-SacPostPayload {
|
|||||||
if ([string]::IsNullOrWhiteSpace($ingest)) { return $false }
|
if ([string]::IsNullOrWhiteSpace($ingest)) { return $false }
|
||||||
|
|
||||||
$jsonText = Repair-SacJsonText -Text (Get-SacSingleString -Value $JsonBody -Label 'spool payload')
|
$jsonText = Repair-SacJsonText -Text (Get-SacSingleString -Value $JsonBody -Label 'spool payload')
|
||||||
$eventId = Get-SacEventIdFromJsonText -JsonText $jsonText
|
if ($jsonText -notmatch '"event_id"\s*:\s*"([0-9a-fA-F-]{36})"') {
|
||||||
if (-not $eventId) {
|
Write-SacLog 'WARN: SAC JSON has no event_id (uuid); skip POST'
|
||||||
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'
|
|
||||||
}
|
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
$eventId = $Matches[1]
|
||||||
$eventType = 'unknown'
|
$eventType = 'unknown'
|
||||||
if ($jsonText -match '"type"\s*:\s*"([^"]+)"') {
|
if ($jsonText -match '"type"\s*:\s*"([^"]+)"') {
|
||||||
$eventType = $Matches[1]
|
$eventType = $Matches[1]
|
||||||
}
|
}
|
||||||
$timeout = Get-SacTimeoutSecResolved
|
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||||
$spoolOnFailure = $true
|
$spoolOnFailure = $true
|
||||||
$bodyBytes = $null
|
$bodyBytes = $null
|
||||||
|
|
||||||
@@ -811,31 +726,6 @@ function Test-SacHeartbeatOnlyEventType {
|
|||||||
return ($EventType -eq 'agent.heartbeat')
|
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 {
|
function Merge-SacNotifyDetails {
|
||||||
param(
|
param(
|
||||||
[hashtable]$Details = $null,
|
[hashtable]$Details = $null,
|
||||||
@@ -908,20 +798,6 @@ function Send-NotifyOrSac {
|
|||||||
return (Send-SacEvent @sacEventArgs)
|
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) {
|
switch ($mode) {
|
||||||
'off' {
|
'off' {
|
||||||
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
||||||
@@ -953,11 +829,7 @@ function Send-NotifyOrSac {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-SacFlushSpool {
|
function Invoke-SacFlushSpool {
|
||||||
param([int]$MaxFiles = 0)
|
param([int]$MaxFiles = 20)
|
||||||
|
|
||||||
if ($MaxFiles -le 0) {
|
|
||||||
$MaxFiles = Get-SacSpoolFlushMaxFilesResolved
|
|
||||||
}
|
|
||||||
|
|
||||||
$mode = Get-SacNormalizedMode
|
$mode = Get-SacNormalizedMode
|
||||||
if ($mode -eq 'off') { return }
|
if ($mode -eq 'off') { return }
|
||||||
@@ -966,46 +838,15 @@ function Invoke-SacFlushSpool {
|
|||||||
$dir = Get-SacSpoolDirResolved
|
$dir = Get-SacSpoolDirResolved
|
||||||
if (-not (Test-Path -LiteralPath $dir)) { return }
|
if (-not (Test-Path -LiteralPath $dir)) { return }
|
||||||
|
|
||||||
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue)
|
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime)
|
||||||
$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)
|
|
||||||
)
|
|
||||||
$count = 0
|
$count = 0
|
||||||
foreach ($f in $ordered) {
|
foreach ($f in $files) {
|
||||||
$count++
|
$count++
|
||||||
if ($count -gt $MaxFiles) { break }
|
if ($count -gt $MaxFiles) { break }
|
||||||
try {
|
try {
|
||||||
$bytes = Read-SacSpoolFileBytes -Path $f.FullName
|
$utf8 = New-Object System.Text.UTF8Encoding $false
|
||||||
if (Test-SacSpoolBytesCorrupt -Bytes $bytes) {
|
$json = [System.IO.File]::ReadAllText($f.FullName, $utf8)
|
||||||
Move-SacSpoolFileToRejected -SpoolFilePath $f.FullName -Reason 'null or empty payload'
|
Invoke-SacPostPayload -JsonBody $json | Out-Null
|
||||||
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
|
|
||||||
} catch {
|
} catch {
|
||||||
Write-SacLog "WARN: SAC spool flush failed for $($f.Name): $($_.Exception.Message)"
|
Write-SacLog "WARN: SAC spool flush failed for $($f.Name): $($_.Exception.Message)"
|
||||||
}
|
}
|
||||||
@@ -1028,7 +869,7 @@ function Get-SacAgentCommandResultUrl {
|
|||||||
function Invoke-SacHttpGet {
|
function Invoke-SacHttpGet {
|
||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Uri,
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
[int]$TimeoutSec = 45
|
[int]$TimeoutSec = 12
|
||||||
)
|
)
|
||||||
Invoke-SacTlsPrep
|
Invoke-SacTlsPrep
|
||||||
try {
|
try {
|
||||||
@@ -1054,7 +895,7 @@ function Invoke-SacHttpPostJson {
|
|||||||
param(
|
param(
|
||||||
[Parameter(Mandatory = $true)][string]$Uri,
|
[Parameter(Mandatory = $true)][string]$Uri,
|
||||||
[Parameter(Mandatory = $true)][string]$JsonBody,
|
[Parameter(Mandatory = $true)][string]$JsonBody,
|
||||||
[int]$TimeoutSec = 45
|
[int]$TimeoutSec = 12
|
||||||
)
|
)
|
||||||
Invoke-SacTlsPrep
|
Invoke-SacTlsPrep
|
||||||
$bytes = Get-SacUtf8Bytes -Text $JsonBody
|
$bytes = Get-SacUtf8Bytes -Text $JsonBody
|
||||||
@@ -1183,7 +1024,7 @@ function Submit-SacAgentCommandResult {
|
|||||||
stdout = $Stdout
|
stdout = $Stdout
|
||||||
stderr = $Stderr
|
stderr = $Stderr
|
||||||
} | ConvertTo-Json -Compress
|
} | ConvertTo-Json -Compress
|
||||||
$timeout = Get-SacTimeoutSecResolved
|
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||||
$resp = Invoke-SacHttpPostJson -Uri $url -JsonBody $body -TimeoutSec $timeout
|
$resp = Invoke-SacHttpPostJson -Uri $url -JsonBody $body -TimeoutSec $timeout
|
||||||
return ($resp.StatusCode -in 200, 201)
|
return ($resp.StatusCode -in 200, 201)
|
||||||
}
|
}
|
||||||
@@ -1196,7 +1037,7 @@ function Invoke-SacProcessPendingCommands {
|
|||||||
if ([string]::IsNullOrWhiteSpace($pollUrl)) { return 0 }
|
if ([string]::IsNullOrWhiteSpace($pollUrl)) { return 0 }
|
||||||
|
|
||||||
$agentId = Get-SacAgentInstanceId
|
$agentId = Get-SacAgentInstanceId
|
||||||
$timeout = Get-SacTimeoutSecResolved
|
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||||
$uri = "${pollUrl}?agent_instance_id=$([uri]::EscapeDataString($agentId))"
|
$uri = "${pollUrl}?agent_instance_id=$([uri]::EscapeDataString($agentId))"
|
||||||
$get = Invoke-SacHttpGet -Uri $uri -TimeoutSec $timeout
|
$get = Invoke-SacHttpGet -Uri $uri -TimeoutSec $timeout
|
||||||
if ($get.StatusCode -ne 200) {
|
if ($get.StatusCode -ne 200) {
|
||||||
|
|||||||
@@ -28,15 +28,15 @@ switch ($Target) {
|
|||||||
$BaseHost = $Base -replace '^https://', ''
|
$BaseHost = $Base -replace '^https://', ''
|
||||||
|
|
||||||
$patterns = @(
|
$patterns = @(
|
||||||
@{ From = 'https://git.papatramp.ru/PTah/([^)/''"\s]+)/blob/main/'; To = "$Base/`$1$BlobSuffix/" }
|
@{ From = 'https://git.kalinamall.ru/PapaTramp/([^)/''"\s]+)/blob/main/'; To = "$Base/`$1$BlobSuffix/" }
|
||||||
@{ From = 'https://git.papatramp.ru/PTah/([^)/''"\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.papatramp.ru/PTah/([^)/''"\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.papatramp.ru/PTah/'; To = "$Base/" }
|
@{ From = 'https://git.kalinamall.ru/PapaTramp/'; To = "$Base/" }
|
||||||
@{ From = 'https://git.papatramp.ru/PTah/'; To = "$Base/" }
|
@{ From = 'https://git.kalinamall.ru/PapaTramp/'; To = "$Base/" }
|
||||||
@{ From = 'https://git.papatramp.ru/PTah/'; To = "$Base/" }
|
@{ From = 'https://git.kalinamall.ru/PapaTramp/'; To = "$Base/" }
|
||||||
@{ From = 'git.papatramp.ru/PTah/'; To = "$BaseHost/" }
|
@{ From = 'git.kalinamall.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||||
@{ From = 'git.papatramp.ru/PTah/'; To = "$BaseHost/" }
|
@{ From = 'git.kalinamall.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||||
@{ From = 'git.papatramp.ru/PTah/'; To = "$BaseHost/" }
|
@{ From = 'git.kalinamall.ru/PapaTramp/'; To = "$BaseHost/" }
|
||||||
)
|
)
|
||||||
|
|
||||||
$extensions = @('*.md', '*.json', '*.service', '*.example', '*.sh', '*.ps1', '*.yml', '*.yaml')
|
$extensions = @('*.md', '*.json', '*.service', '*.example', '*.sh', '*.ps1', '*.yml', '*.yaml')
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
2.1.9-SAC
|
2.1.2-SAC
|
||||||
|
|||||||
Reference in New Issue
Block a user