Compare commits
32 Commits
1.4.3
..
1684f60fd1
| Author | SHA1 | Date | |
|---|---|---|---|
| 1684f60fd1 | |||
| 3d9df80c7c | |||
| 942b353f56 | |||
| 1120c27709 | |||
| 74ebbbe161 | |||
| 5d9176f958 | |||
| eff265ded3 | |||
| 39f0e6098c | |||
| fcff8311d4 | |||
| 8de023f59e | |||
| e14e95922a | |||
| 3f97da0c2e | |||
| d0b02042a4 | |||
| b16ebb028b | |||
| 66437bf381 | |||
| b9027009b8 | |||
| 0d52083a58 | |||
| ab0504c3f9 | |||
| d8ab28098e | |||
| 4386a58942 | |||
| d951f54874 | |||
| 4bef73001f | |||
| 4f6385d483 | |||
| 0ce6b8abde | |||
| 6072d70e59 | |||
| 7b9d7dcd40 | |||
| b39d9c0deb | |||
| 0e5196e769 | |||
| 578230f915 | |||
| a337e72a4d | |||
| 50a5f87603 | |||
| 2ae691f83c |
@@ -34,13 +34,6 @@
|
|||||||
|
|
||||||
Лог установки: **`C:\ProgramData\RDP-login-monitor\Logs\deploy.log`**.
|
Лог установки: **`C:\ProgramData\RDP-login-monitor\Logs\deploy.log`**.
|
||||||
|
|
||||||
## Опционально на клиенте: `ignore.lst`
|
|
||||||
|
|
||||||
На каждом компьютере/сервере можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (в том же каталоге, что и **`Login_Monitor.ps1`**). По строкам этого файла монитор **не отправляет в Telegram** отдельные события **Security `4624`/`4625`** (например, шум от известной рабочей станции, тестового пользователя или фиксированного IP).
|
|
||||||
|
|
||||||
- **`Deploy-LoginMonitor.ps1` с шары `ignore.lst` не доставляет** — при необходимости создавайте файл локально или копируйте своим способом.
|
|
||||||
- Примеры синтаксиса — в репозитории в файле **`ignore.lst.example`**.
|
|
||||||
|
|
||||||
## Задачи планировщика после `-InstallTasks`
|
## Задачи планировщика после `-InstallTasks`
|
||||||
|
|
||||||
Параметр **`Login_Monitor.ps1 -InstallTasks`** создаёт две задачи:
|
Параметр **`Login_Monitor.ps1 -InstallTasks`** создаёт две задачи:
|
||||||
|
|||||||
+1
-42
@@ -33,22 +33,9 @@ param(
|
|||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
function Test-DeployRunningElevated {
|
|
||||||
try {
|
|
||||||
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
||||||
# LocalSystem (GPO startup / задачи SYSTEM): не всегда даёт true на BuiltInRole::Administrator.
|
|
||||||
if ($null -ne $id.User -and $id.User.Value -eq 'S-1-5-18') { return $true }
|
|
||||||
$p = New-Object Security.Principal.WindowsPrincipal($id)
|
|
||||||
return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
|
||||||
} catch {
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
|
$InstallRoot = [System.IO.Path]::GetFullPath("$env:ProgramData\RDP-login-monitor")
|
||||||
$LocalScript = Join-Path $InstallRoot "Login_Monitor.ps1"
|
$LocalScript = Join-Path $InstallRoot "Login_Monitor.ps1"
|
||||||
$VersionStampPath = Join-Path $InstallRoot "deployed_version.txt"
|
$VersionStampPath = Join-Path $InstallRoot "deployed_version.txt"
|
||||||
$DeployUpdateMarkerPath = Join-Path $InstallRoot "deploy_last_update.txt"
|
|
||||||
$DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log"
|
$DeployLogPath = Join-Path $InstallRoot "Logs\deploy.log"
|
||||||
$ScriptName = "Login_Monitor.ps1"
|
$ScriptName = "Login_Monitor.ps1"
|
||||||
$VersionFileName = "version.txt"
|
$VersionFileName = "version.txt"
|
||||||
@@ -218,11 +205,6 @@ try {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not (Test-DeployRunningElevated)) {
|
|
||||||
Write-DeployLog "ОШИБКА: запустите Deploy из повышенной консоли PowerShell («Запуск от имени администратора»). Без этого дочерний Login_Monitor.ps1 -InstallTasks завершится с кодом 1 (нет прав на регистрацию задач)."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
Stop-RdpLoginMonitorMainProcesses
|
Stop-RdpLoginMonitorMainProcesses
|
||||||
|
|
||||||
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
|
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
|
||||||
@@ -231,23 +213,9 @@ try {
|
|||||||
$installArgs = @(
|
$installArgs = @(
|
||||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
|
||||||
)
|
)
|
||||||
$instOut = Join-Path $InstallRoot "Logs\deploy_installtasks_stdout.log"
|
$p = Start-Process -FilePath $PsExe -ArgumentList $installArgs -Wait -PassThru -WindowStyle Hidden
|
||||||
$instErr = Join-Path $InstallRoot "Logs\deploy_installtasks_stderr.log"
|
|
||||||
$p = Start-Process -FilePath $PsExe -ArgumentList $installArgs -Wait -PassThru -WindowStyle Hidden `
|
|
||||||
-RedirectStandardOutput $instOut -RedirectStandardError $instErr
|
|
||||||
if ($p.ExitCode -ne 0) {
|
if ($p.ExitCode -ne 0) {
|
||||||
Write-DeployLog "Предупреждение: InstallTasks завершился с кодом $($p.ExitCode)."
|
Write-DeployLog "Предупреждение: InstallTasks завершился с кодом $($p.ExitCode)."
|
||||||
foreach ($pair in @(@($instOut, 'stdout'), @($instErr, 'stderr'))) {
|
|
||||||
$lp = $pair[0]
|
|
||||||
$lbl = $pair[1]
|
|
||||||
if (Test-Path -LiteralPath $lp) {
|
|
||||||
$tail = Get-Content -LiteralPath $lp -Tail 40 -ErrorAction SilentlyContinue
|
|
||||||
if ($tail) {
|
|
||||||
Write-DeployLog "InstallTasks $lbl (хвост): $($tail -join ' | ')"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Write-DeployLog "Подсказка: полный вывод в Logs\deploy_installtasks_*.log; также см. login_monitor.log за это время."
|
|
||||||
} else {
|
} else {
|
||||||
Write-DeployLog "InstallTasks выполнен (код 0)."
|
Write-DeployLog "InstallTasks выполнен (код 0)."
|
||||||
}
|
}
|
||||||
@@ -255,15 +223,6 @@ try {
|
|||||||
[System.IO.File]::WriteAllText($VersionStampPath, "$shareVerRaw`r`n", $Utf8Bom)
|
[System.IO.File]::WriteAllText($VersionStampPath, "$shareVerRaw`r`n", $Utf8Bom)
|
||||||
Write-DeployLog "Записана метка версии: $VersionStampPath"
|
Write-DeployLog "Записана метка версии: $VersionStampPath"
|
||||||
|
|
||||||
$updStamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
|
|
||||||
$updMarker = @(
|
|
||||||
"Version=$shareVerRaw"
|
|
||||||
"UpdatedAt=$updStamp"
|
|
||||||
"PendingStartupNotice=1"
|
|
||||||
) -join "`r`n"
|
|
||||||
[System.IO.File]::WriteAllText($DeployUpdateMarkerPath, "$updMarker`r`n", $Utf8Bom)
|
|
||||||
Write-DeployLog "Записана метка обновления: $DeployUpdateMarkerPath (Version=$shareVerRaw; UpdatedAt=$updStamp)."
|
|
||||||
|
|
||||||
if (-not $SkipStartMonitorAfterUpdate) {
|
if (-not $SkipStartMonitorAfterUpdate) {
|
||||||
Start-Process -FilePath $PsExe -ArgumentList @(
|
Start-Process -FilePath $PsExe -ArgumentList @(
|
||||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2026 Andrey Lutsenko (PapaTramp)
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
+21
-231
@@ -69,7 +69,7 @@ $script:MonitorSingletonLockStream = $null
|
|||||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||||
$ScriptVersion = "1.4.3"
|
$ScriptVersion = "1.3.12"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -83,11 +83,6 @@ $LogRotationMinute = 0
|
|||||||
# Heartbeat (только файл)
|
# Heartbeat (только файл)
|
||||||
$HeartbeatInterval = 3600
|
$HeartbeatInterval = 3600
|
||||||
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
$HeartbeatFile = Join-Path $script:InstallRoot "Logs\last_heartbeat.txt"
|
||||||
$DeployUpdateMarkerFile = Join-Path $script:InstallRoot "deploy_last_update.txt"
|
|
||||||
# Построчные правила подавления уведомлений Security 4624/4625 (см. ignore.lst.example в репозитории).
|
|
||||||
$script:IgnoreListPath = Join-Path $script:InstallRoot "ignore.lst"
|
|
||||||
$script:IgnoreListCache = $null
|
|
||||||
$script:IgnoreListCacheStampUtc = $null
|
|
||||||
|
|
||||||
# Ежедневный отчет
|
# Ежедневный отчет
|
||||||
$DailyReportHour = 9
|
$DailyReportHour = 9
|
||||||
@@ -491,24 +486,6 @@ function Start-RdpMonitorWatchdogMain {
|
|||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Watchdog и InstallTasks — до DPAPI/Telegram: Deploy вызывает -InstallTasks без зависимости от секретов на машине.
|
|
||||||
if ($Watchdog) {
|
|
||||||
Start-RdpMonitorWatchdogMain
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($InstallTasks) {
|
|
||||||
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
||||||
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
|
||||||
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
|
||||||
Write-Log "InstallTasks: нужны права администратора."
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
Register-RdpMonitorScheduledTasksCore
|
|
||||||
Write-Log "InstallTasks: задачи планировщика обновлены."
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# --- Учётные данные Telegram (открытый текст или DPAPI Base64) ---
|
# --- Учётные данные Telegram (открытый текст или DPAPI Base64) ---
|
||||||
if (-not [string]::IsNullOrWhiteSpace($TelegramBotTokenProtectedB64)) {
|
if (-not [string]::IsNullOrWhiteSpace($TelegramBotTokenProtectedB64)) {
|
||||||
try {
|
try {
|
||||||
@@ -529,6 +506,23 @@ if (-not [string]::IsNullOrWhiteSpace($TelegramChatIDProtectedB64)) {
|
|||||||
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" }
|
if ($TelegramBotToken -eq '<TELEGRAM_BOT_TOKEN>') { $TelegramBotToken = "" }
|
||||||
if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" }
|
if ($TelegramChatID -eq '<TELEGRAM_CHAT_ID>') { $TelegramChatID = "" }
|
||||||
|
|
||||||
|
if ($Watchdog) {
|
||||||
|
Start-RdpMonitorWatchdogMain
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($InstallTasks) {
|
||||||
|
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = New-Object Security.Principal.WindowsPrincipal($currentUser)
|
||||||
|
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
|
||||||
|
Write-Log "InstallTasks: нужны права администратора."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Register-RdpMonitorScheduledTasksCore
|
||||||
|
Write-Log "InstallTasks: задачи планировщика обновлены."
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
function ConvertTo-TelegramHtml {
|
function ConvertTo-TelegramHtml {
|
||||||
param([string]$Text)
|
param([string]$Text)
|
||||||
if ($null -eq $Text) { return '' }
|
if ($null -eq $Text) { return '' }
|
||||||
@@ -889,53 +883,9 @@ function Send-Heartbeat {
|
|||||||
$timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
|
$timestamp = Get-Date -Format "dd.MM.yyyy HH:mm:ss"
|
||||||
$hHost = (ConvertTo-TelegramHtml $env:COMPUTERNAME)
|
$hHost = (ConvertTo-TelegramHtml $env:COMPUTERNAME)
|
||||||
|
|
||||||
function Get-DeployUpdateMarker {
|
|
||||||
$info = [pscustomobject]@{
|
|
||||||
Version = $null
|
|
||||||
UpdatedAt = $null
|
|
||||||
PendingStartupNotice = $false
|
|
||||||
}
|
|
||||||
if (-not (Test-Path -LiteralPath $DeployUpdateMarkerFile)) { return $info }
|
|
||||||
try {
|
|
||||||
$lines = @((Get-Content -LiteralPath $DeployUpdateMarkerFile -ErrorAction Stop) | Where-Object { $_ -match '\S' })
|
|
||||||
foreach ($ln in $lines) {
|
|
||||||
$parts = $ln -split '=', 2
|
|
||||||
if ($parts.Count -ne 2) { continue }
|
|
||||||
$k = ($parts[0]).Trim()
|
|
||||||
$v = ($parts[1]).Trim()
|
|
||||||
switch ($k) {
|
|
||||||
'Version' { $info.Version = $v }
|
|
||||||
'UpdatedAt' { $info.UpdatedAt = $v }
|
|
||||||
'PendingStartupNotice' { $info.PendingStartupNotice = ($v -eq '1' -or $v -ieq 'true') }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch { }
|
|
||||||
return $info
|
|
||||||
}
|
|
||||||
|
|
||||||
function Set-DeployUpdateMarkerPendingOff {
|
|
||||||
param([pscustomobject]$Marker)
|
|
||||||
try {
|
|
||||||
$ver = if ([string]::IsNullOrWhiteSpace($Marker.Version)) { '' } else { $Marker.Version }
|
|
||||||
$upd = if ([string]::IsNullOrWhiteSpace($Marker.UpdatedAt)) { '' } else { $Marker.UpdatedAt }
|
|
||||||
$content = @(
|
|
||||||
"Version=$ver"
|
|
||||||
"UpdatedAt=$upd"
|
|
||||||
"PendingStartupNotice=0"
|
|
||||||
) -join "`r`n"
|
|
||||||
Write-TextFileUtf8Bom -Path $DeployUpdateMarkerFile -Text $content
|
|
||||||
} catch { }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($IsStartup) {
|
if ($IsStartup) {
|
||||||
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
|
$message = "<b>✅ Мониторинг логинов ЗАПУЩЕН</b>`r`n"
|
||||||
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)"
|
$message += "🏷️ Версия скрипта: $(ConvertTo-TelegramHtml $ScriptVersion)`r`n"
|
||||||
$upd = Get-DeployUpdateMarker
|
|
||||||
if ($upd.PendingStartupNotice -and $upd.Version -eq $ScriptVersion -and -not [string]::IsNullOrWhiteSpace($upd.UpdatedAt)) {
|
|
||||||
$message += " (обновлён $(ConvertTo-TelegramHtml $upd.UpdatedAt))"
|
|
||||||
Set-DeployUpdateMarkerPendingOff -Marker $upd
|
|
||||||
}
|
|
||||||
$message += "`r`n"
|
|
||||||
$message += "🖥️ Сервер: $hHost`r`n"
|
$message += "🖥️ Сервер: $hHost`r`n"
|
||||||
$message += "🕐 Время запуска: $timestamp"
|
$message += "🕐 Время запуска: $timestamp"
|
||||||
if ($script:OsInstallKindLabel) {
|
if ($script:OsInstallKindLabel) {
|
||||||
@@ -946,23 +896,6 @@ function Send-Heartbeat {
|
|||||||
} else {
|
} else {
|
||||||
$message += "`r`n📌 <b>Режим:</b> сервер — Security 4624/4625, типы входа 2, 3, 10."
|
$message += "`r`n📌 <b>Режим:</b> сервер — Security 4624/4625, типы входа 2, 3, 10."
|
||||||
}
|
}
|
||||||
$ignoreEntries = @(Get-RdpMonitorIgnoreListEntries)
|
|
||||||
if ($ignoreEntries.Count -gt 0) {
|
|
||||||
$message += "`r`n🚫 <b>Игнорируются:</b> Security 4624/4625 по правилам ignore.lst`r`n"
|
|
||||||
foreach ($e in $ignoreEntries) {
|
|
||||||
$v = ConvertTo-TelegramHtml ([string]$e.Value)
|
|
||||||
$kindLabel = switch ($e.Kind) {
|
|
||||||
'User' { 'Пользователь' }
|
|
||||||
'Workstation' { 'Рабочая станция' }
|
|
||||||
'Ip' { 'IP' }
|
|
||||||
'Any' { 'Универсальное правило' }
|
|
||||||
default { 'Правило' }
|
|
||||||
}
|
|
||||||
$message += ('• {0}: {1}' -f $kindLabel, $v) + "`r`n"
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$message += "`r`n🚫 <b>Игнорируются:</b> не задано (ignore.lst отсутствует или пуст)."
|
|
||||||
}
|
|
||||||
if (Test-RDSDeploymentPresent) {
|
if (Test-RDSDeploymentPresent) {
|
||||||
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
|
$message += "`r`n🔐 <b>RDS (хост сессий):</b> обнаружены компоненты RDS помимо чистого шлюза — в мониторинг входят входы по RDP/RDS на этом узле (Security 4624/4625, типы входа по настройке скрипта)."
|
||||||
}
|
}
|
||||||
@@ -1127,137 +1060,6 @@ function Get-LogonTypeName {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function Test-RdpMonitorUsernameMatchesToken {
|
|
||||||
param([string]$Username, [string]$Token)
|
|
||||||
if ([string]::IsNullOrWhiteSpace($Username) -or [string]::IsNullOrWhiteSpace($Token)) { return $false }
|
|
||||||
if ($Username -ieq $Token) { return $true }
|
|
||||||
if ($Token.Contains('\')) {
|
|
||||||
return ($Username -ieq $Token)
|
|
||||||
}
|
|
||||||
$i = $Username.LastIndexOf('\')
|
|
||||||
if ($i -ge 0 -and $i -lt ($Username.Length - 1)) {
|
|
||||||
$sam = $Username.Substring($i + 1)
|
|
||||||
if ($sam -ieq $Token) { return $true }
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Parse-RdpMonitorIgnoreListLine {
|
|
||||||
param([string]$RawLine)
|
|
||||||
$line = ([string]$RawLine).Trim()
|
|
||||||
if ($line.Length -eq 0) { return $null }
|
|
||||||
if ($line[0] -eq '#' -or $line[0] -eq ';') { return $null }
|
|
||||||
if ($line.StartsWith([char]0xFEFF)) { $line = $line.TrimStart([char]0xFEFF) }
|
|
||||||
|
|
||||||
if ($line -notmatch ':') {
|
|
||||||
return [pscustomobject]@{ Kind = 'Any'; Value = $line }
|
|
||||||
}
|
|
||||||
|
|
||||||
$idx = $line.IndexOf(':')
|
|
||||||
$left = $line.Substring(0, $idx).Trim()
|
|
||||||
$right = $line.Substring($idx + 1).Trim()
|
|
||||||
if ([string]::IsNullOrWhiteSpace($right)) { return $null }
|
|
||||||
|
|
||||||
if ($left -match '(?i)(рабоч|workstation|wks)') {
|
|
||||||
return [pscustomobject]@{ Kind = 'Workstation'; Value = $right }
|
|
||||||
}
|
|
||||||
if ($left -match '(?i)(польз|username|subject|account|target\s*user|\buser\b)') {
|
|
||||||
return [pscustomobject]@{ Kind = 'User'; Value = $right }
|
|
||||||
}
|
|
||||||
if ($left -match '(?i)(\bip\b|ip\s*адрес|ipaddress|адрес\s*ip)') {
|
|
||||||
return [pscustomobject]@{ Kind = 'Ip'; Value = $right }
|
|
||||||
}
|
|
||||||
|
|
||||||
return [pscustomobject]@{ Kind = 'Any'; Value = $right }
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-RdpMonitorIgnoreListEntries {
|
|
||||||
if (-not (Test-Path -LiteralPath $script:IgnoreListPath)) {
|
|
||||||
$script:IgnoreListCache = @()
|
|
||||||
$script:IgnoreListCacheStampUtc = $null
|
|
||||||
return $script:IgnoreListCache
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
$fi = Get-Item -LiteralPath $script:IgnoreListPath -ErrorAction Stop
|
|
||||||
$stamp = $fi.LastWriteTimeUtc
|
|
||||||
if ($null -ne $script:IgnoreListCache -and $script:IgnoreListCacheStampUtc -eq $stamp) {
|
|
||||||
return $script:IgnoreListCache
|
|
||||||
}
|
|
||||||
$entries = [System.Collections.Generic.List[object]]::new()
|
|
||||||
foreach ($ln in (Get-Content -LiteralPath $script:IgnoreListPath -Encoding UTF8 -ErrorAction Stop)) {
|
|
||||||
$e = Parse-RdpMonitorIgnoreListLine -RawLine $ln
|
|
||||||
if ($null -ne $e) { $entries.Add($e) | Out-Null }
|
|
||||||
}
|
|
||||||
$script:IgnoreListCache = @($entries)
|
|
||||||
$script:IgnoreListCacheStampUtc = $stamp
|
|
||||||
$arr = $script:IgnoreListCache
|
|
||||||
$nIp = @($arr | Where-Object { $_.Kind -eq 'Ip' }).Count
|
|
||||||
$nUser = @($arr | Where-Object { $_.Kind -eq 'User' }).Count
|
|
||||||
$nWks = @($arr | Where-Object { $_.Kind -eq 'Workstation' }).Count
|
|
||||||
$nAny = @($arr | Where-Object { $_.Kind -eq 'Any' }).Count
|
|
||||||
$nTotal = $arr.Count
|
|
||||||
if ($nTotal -eq 0) {
|
|
||||||
Write-Log "ignore.lst обновлён: список правил пуст, игнорирование по файлу для Security 4624/4625 не задаётся."
|
|
||||||
} else {
|
|
||||||
Write-Log ("ignore.lst обновлён: добавлено игнорирование событий 4624/4625 по IP ({0}), пользователю ({1}), рабочей станции ({2}); универсальных правил ({3}). Всего записей: {4}." -f $nIp, $nUser, $nWks, $nAny, $nTotal)
|
|
||||||
}
|
|
||||||
return $script:IgnoreListCache
|
|
||||||
} catch {
|
|
||||||
Write-Log "Предупреждение: не удалось прочитать ignore.lst: $($_.Exception.Message)"
|
|
||||||
$script:IgnoreListCache = @()
|
|
||||||
$script:IgnoreListCacheStampUtc = $null
|
|
||||||
return $script:IgnoreListCache
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Test-RdpMonitorIgnoreListMatch {
|
|
||||||
param(
|
|
||||||
[string]$Username,
|
|
||||||
[string]$ComputerName,
|
|
||||||
[string]$SourceIP
|
|
||||||
)
|
|
||||||
$entries = @(Get-RdpMonitorIgnoreListEntries)
|
|
||||||
if ($entries.Count -eq 0) { return $false }
|
|
||||||
|
|
||||||
foreach ($e in $entries) {
|
|
||||||
$v = [string]$e.Value
|
|
||||||
if ([string]::IsNullOrWhiteSpace($v)) { continue }
|
|
||||||
|
|
||||||
switch ($e.Kind) {
|
|
||||||
'User' {
|
|
||||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
|
||||||
}
|
|
||||||
'Workstation' {
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'Ip' {
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
'Any' {
|
|
||||||
if (Test-RdpMonitorStringLooksLikeIPv4 $v) {
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($SourceIP) -and $SourceIP -ne '-' -and ($SourceIP -ieq $v)) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if ($v.Contains('\')) {
|
|
||||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if (-not [string]::IsNullOrWhiteSpace($ComputerName) -and $ComputerName -ne '-' -and ($ComputerName -ieq $v)) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
if (Test-RdpMonitorUsernameMatchesToken -Username $Username -Token $v) { return $true }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return $false
|
|
||||||
}
|
|
||||||
|
|
||||||
function Should-IgnoreEvent {
|
function Should-IgnoreEvent {
|
||||||
param(
|
param(
|
||||||
[string]$Username,
|
[string]$Username,
|
||||||
@@ -1315,12 +1117,6 @@ function Should-IgnoreEvent {
|
|||||||
if ($ComputerName -like $pattern) { return $true }
|
if ($ComputerName -like $pattern) { return $true }
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($EventID -in 4624, 4625) {
|
|
||||||
if (Test-RdpMonitorIgnoreListMatch -Username $Username -ComputerName $ComputerName -SourceIP $SourceIP) {
|
|
||||||
return $true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $false
|
return $false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1546,14 +1342,8 @@ function Format-RDGatewayEvent {
|
|||||||
$hTime = (ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')))
|
$hTime = (ConvertTo-TelegramHtml ($TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')))
|
||||||
|
|
||||||
$message = "<b>"
|
$message = "<b>"
|
||||||
if ($EventID -eq 302) { $message += "🖥️ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
|
if ($EventID -eq 302) { $message += "🖥️ УСПЕШНОЕ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
|
||||||
elseif ($EventID -eq 303) {
|
elseif ($EventID -eq 303) { $message += "❌ НЕУДАЧНОЕ ПОДКЛЮЧЕНИЕ ЧЕРЕЗ RD GATEWAY" }
|
||||||
if ($ErrorCode -ne "0" -and $ErrorCode -ne "N/A" -and -not [string]::IsNullOrWhiteSpace($ErrorCode)) {
|
|
||||||
$message += "⚠️ СЕАНС ЧЕРЕЗ RD GATEWAY ЗАВЕРШЁН С ОШИБКОЙ"
|
|
||||||
} else {
|
|
||||||
$message += "ℹ️ СЕАНС ЧЕРЕЗ RD GATEWAY ЗАВЕРШЁН"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else { $message += "⚠️ СОБЫТИЕ RD GATEWAY" }
|
else { $message += "⚠️ СОБЫТИЕ RD GATEWAY" }
|
||||||
$message += "</b>`r`n"
|
$message += "</b>`r`n"
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,14 @@
|
|||||||
# RDP Login Monitor
|
# RDP Login Monitor
|
||||||
|
|
||||||
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram.
|
PowerShell-набор для мониторинга входов в Windows с отправкой уведомлений в Telegram.
|
||||||
|
|
||||||
## Актуальная схема (рекомендуется)
|
## Актуальная схема (рекомендуется)
|
||||||
|
|
||||||
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**.
|
- **`Login_Monitor.ps1`** — единый монитор: журнал Security (`4624`/`4625`), при необходимости RD Gateway (`302`/`303`), рабочие станции и серверы, ротация логов, heartbeat, отчёты. Устанавливается в **`C:\ProgramData\RDP-login-monitor\`**, задачи **`RDP-Login-Monitor`** и **`RDP-Login-Monitor-Watchdog`** создаются параметром **`-InstallTasks`** (watchdog через **`schtasks`**, см. **DEPLOY.md**).
|
||||||
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway** — **`302`/`303`**, **ежедневный отчёт** в Telegram (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram.
|
- **`Deploy-LoginMonitor.ps1`** + **`version.txt`** — доставка с файловой шары по версии (домен, GPO «автозагрузка» компьютера). Подробности, структура шары и правила версий: **[DEPLOY.md](DEPLOY.md)**.
|
||||||
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт:
|
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально, для DPAPI-строк токена/chat id на конкретном ПК.
|
||||||
- `RDP-Login-Monitor` (основной монитор),
|
|
||||||
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
|
Разделы ниже про **`D:\Soft\`**, **`Watchdog_RDP_Monitor.ps1`** и **`Install-ScheduledTasks.ps1`** относятся к **старой схеме** и оставлены для совместимости; новые установки ориентируйте на **`ProgramData`** и **DEPLOY.md**.
|
||||||
- Доменная доставка и обновления: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** с шары `NETLOGON`. После успешного деплоя в приветственном сообщении Telegram может появиться отметка об обновлении (файл **`deploy_last_update.txt`** рядом с логами).
|
|
||||||
- Для полной инструкции по деплою/GPO используйте **[DEPLOY.md](DEPLOY.md)**.
|
|
||||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id.
|
|
||||||
|
|
||||||
## Что изменилось (важное)
|
## Что изменилось (важное)
|
||||||
|
|
||||||
@@ -22,109 +19,86 @@ PowerShell-набор для мониторинга входов в Windows с
|
|||||||
|
|
||||||
## 1) Подготовка
|
## 1) Подготовка
|
||||||
|
|
||||||
1. Подготовьте папку установки:
|
1. Скопируйте в `D:\Soft\` как минимум `Login_Monitor.ps1` и `Watchdog_RDP_Monitor.ps1` (для установки из скрипта — ещё `Install-ScheduledTasks.ps1`), либо измените пути в параметрах установщика / в watchdog.
|
||||||
- `C:\ProgramData\RDP-login-monitor\`
|
2. Откройте `Login_Monitor.ps1` и задайте:
|
||||||
2. Скопируйте в неё как минимум:
|
- `$TelegramBotToken`
|
||||||
- `Login_Monitor.ps1`
|
- `$TelegramChatID`
|
||||||
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1` и `version.txt`.
|
3. Убедитесь, что существует `D:\Soft\Logs\` (скрипт сам создаст при запуске).
|
||||||
3. Откройте `Login_Monitor.ps1` и задайте токен/чат:
|
4. Запускайте от имени администратора (нужен доступ к журналу `Security`).
|
||||||
- `$TelegramBotToken` или `...ProtectedB64`
|
|
||||||
- `$TelegramChatID` или `...ProtectedB64`
|
|
||||||
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
|
|
||||||
5. Логи и служебные файлы будут в:
|
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
|
||||||
6. (Опционально) Подавление части алертов по списку — см. раздел **«7) ignore.lst»** ниже.
|
|
||||||
|
|
||||||
## 2) Ручной запуск
|
## 2) Ручной запуск
|
||||||
|
|
||||||
Используйте этот вариант для быстрой проверки старта/логики без установки задач планировщика.
|
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1"
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Login_Monitor.ps1"
|
||||||
```
|
```
|
||||||
|
|
||||||
Примечание: при ручном запуске монитор работает в текущей сессии до остановки (например, `Ctrl+C`).
|
|
||||||
|
|
||||||
## 3) Запуск через Планировщик заданий (Task Scheduler)
|
## 3) Запуск через Планировщик заданий (Task Scheduler)
|
||||||
|
|
||||||
Текущая схема: вручную задачи в GUI создавать не нужно.
|
Задания можно завести **вручную в GUI** (ниже) или **одной командой из PowerShell** — см. `Install-ScheduledTasks.ps1` (в начале файла стоит `#Requires -RunAsAdministrator`). Скрипт регистрирует **`RDP Login Monitor`** (старт ОС, запуск `Login_Monitor.ps1`) и **`RDP Login Monitor Watchdog`** (старт ОС + повтор каждые 5 минут, запуск `Watchdog_RDP_Monitor.ps1` из того же каталога). Пути по умолчанию: `D:\Soft\`. Параметры: `-InstallRoot`, `-MainTaskName`, `-WatchdogTaskName`, `-WatchdogRepeatMinutes`, задержки случайного старта и т.д.
|
||||||
|
|
||||||
Достаточно запустить:
|
Пример после копирования файлов в `D:\Soft\`:
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Install-ScheduledTasks.ps1"
|
||||||
```
|
```
|
||||||
|
|
||||||
Скрипт сам зарегистрирует `RDP-Login-Monitor` и `RDP-Login-Monitor-Watchdog`, а также запросит немедленный первый запуск задач.
|
Это не заменяет настройку токена Telegram в `Login_Monitor.ps1`, а только регистрирует задания.
|
||||||
|
|
||||||
Для доменной установки/обновления с шары вручную ничего в планировщике на клиенте настраивать не требуется: используйте `Deploy-LoginMonitor.ps1` (подробно в `DEPLOY.md`).
|
### Задание №1: основной монитор (вручную)
|
||||||
|
|
||||||
|
Создайте задачу `RDP Login Monitor`:
|
||||||
|
|
||||||
|
- **General**
|
||||||
|
- `Run whether user is logged on or not`
|
||||||
|
- `Run with highest privileges`
|
||||||
|
- `Configure for`: ваша версия Windows Server/Windows
|
||||||
|
- **Triggers**
|
||||||
|
- `At startup` (при запуске системы)
|
||||||
|
- **Actions**
|
||||||
|
- Program/script: `powershell.exe`
|
||||||
|
- Add arguments:
|
||||||
|
```text
|
||||||
|
-NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Login_Monitor.ps1"
|
||||||
|
```
|
||||||
|
- **Settings**
|
||||||
|
- `If the task is already running`: `Do not start a new instance`
|
||||||
|
|
||||||
|
### Задание №2: watchdog (вручную)
|
||||||
|
|
||||||
|
Создайте задачу `RDP Login Monitor Watchdog`:
|
||||||
|
|
||||||
|
- **General**
|
||||||
|
- `Run whether user is logged on or not`
|
||||||
|
- `Run with highest privileges`
|
||||||
|
- **Triggers**
|
||||||
|
- `At startup`
|
||||||
|
- Дополнительно: `Repeat task every 5 minutes` (или отдельный триггер по расписанию каждые 5 минут)
|
||||||
|
- **Actions**
|
||||||
|
- Program/script: `powershell.exe`
|
||||||
|
- Add arguments:
|
||||||
|
```text
|
||||||
|
-NoProfile -ExecutionPolicy Bypass -File "D:\Soft\Watchdog_RDP_Monitor.ps1"
|
||||||
|
```
|
||||||
|
- **Settings**
|
||||||
|
- `If the task is already running`: `Do not start a new instance`
|
||||||
|
|
||||||
|
> Если watchdog лежит не в `D:\Soft\`, поправьте путь в аргументах или параметр `-MainScriptPath`.
|
||||||
|
|
||||||
## 4) Что проверять после запуска
|
## 4) Что проверять после запуска
|
||||||
|
|
||||||
- Логи:
|
- Логи:
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log`
|
- `D:\Soft\Logs\login_monitor.log`
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
|
- `D:\Soft\Logs\watchdog.log`
|
||||||
- Heartbeat:
|
- Heartbeat:
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` обновляется по интервалу **`$HeartbeatInterval`** (по умолчанию раз в час).
|
- `D:\Soft\Logs\last_heartbeat.txt` обновляется примерно раз в час (по `$HeartbeatInterval`).
|
||||||
- Ежедневный отчёт: после первого прохождения дневного слота (по умолчанию **09:00**, задаётся **`$DailyReportHour`** / **`$DailyReportMinute`** в `Login_Monitor.ps1`) в Telegram уходит сводка по **`quser`**; метка последнего отчёта — `Logs\last_daily_report.txt`.
|
|
||||||
- Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий».
|
- Telegram при старте: при установленном **RD Session Host** (или аналогичных компонентах RDS, не только шлюз) — строка про входы по RDP/RDS на этом сервере; при доступном журнале **RD Gateway** — отдельная строка про подключения к **внутренним целевым ПК** через шлюз (302/303). Узел только с ролью RD Gateway не дублирует формулировку «хост сессий».
|
||||||
|
|
||||||
## 5) Автоматический перезапуск при падении
|
## 5) Автоматический перезапуск при падении
|
||||||
|
|
||||||
Режим `-Watchdog` внутри `Login_Monitor.ps1` делает:
|
`Watchdog_RDP_Monitor.ps1` делает:
|
||||||
|
|
||||||
- проверяет, есть ли процесс `powershell.exe/pwsh.exe` с `Login_Monitor.ps1` в командной строке;
|
- проверяет, есть ли процесс `powershell.exe/pwsh.exe` с `Login_Monitor.ps1` в командной строке;
|
||||||
- если процесса нет — запускает монитор;
|
- если процесса нет — запускает монитор;
|
||||||
- если монитор уже есть — не дублирует экземпляр.
|
- если heartbeat старше `$HeartbeatStaleMinutes` (по умолчанию 90 минут) — перезапускает монитор.
|
||||||
|
|
||||||
## 6) Дополнительные параметры и прочие файлы
|
|
||||||
|
|
||||||
- **`-SkipScheduledTaskMaintenance`**: при обычном запуске монитора не выполнять проверку/пересоздание задач планировщика (если регистрацию задач ведёте только через **`-InstallTasks`** или вручную).
|
|
||||||
- **`Install-DeployScheduledTask.ps1`** — helper для периодического запуска **`Deploy-LoginMonitor.ps1`** с шары (см. **[DEPLOY.md](DEPLOY.md)**).
|
|
||||||
- **`Watchdog_RDP_Monitor.ps1`** и **`Install-ScheduledTasks.ps1`** — **альтернативная** схема с отдельным watchdog-файлом и путями по умолчанию **`D:\Soft`**. Для новых установок рекомендуется встроенный режим **`-Watchdog`** в **`Login_Monitor.ps1`** и задачи **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`**.
|
|
||||||
- **`ignore.lst.example`** в репозитории — образец файла **`ignore.lst`** для подавления отдельных уведомлений Security (см. раздел 7).
|
|
||||||
|
|
||||||
## 7) Подавление уведомлений Security: `ignore.lst`
|
|
||||||
|
|
||||||
В каталоге установки можно положить файл **`C:\ProgramData\RDP-login-monitor\ignore.lst`** (рядом с **`Login_Monitor.ps1`**). Правила из списка проверяются **только** для Telegram-уведомлений по событиям **`4624`/`4625`** журнала Security (успех/неудача входа). Жёстко заданные в скрипте исключения (`ExcludedUsers`, локальный IP, сервисные учётные записи и т.д.) по-прежнему действуют для всех типов событий; **`ignore.lst`** добавляет к ним **дополнительные** совпадения именно для **4624/4625**.
|
|
||||||
|
|
||||||
События **RD Gateway (`302`/`303`)**, **RCM `1149`**, ежедневный отчёт и heartbeat **этим файлом не настраиваются** (для `1149` список не используется, даже если формально вызывается общая функция фильтрации).
|
|
||||||
|
|
||||||
### Как читается файл
|
|
||||||
|
|
||||||
- Чтение выполняется по мере обработки событий; содержимое **кэшируется в памяти**. Если **`LastWriteTimeUtc`** файла изменился (редактирование и сохранение), список **перечитывается автоматически** — перезапуск монитора не обязателен.
|
|
||||||
- Кодировка: **UTF-8** (`Get-Content -Encoding UTF8`). Строка может начинаться с BOM — он отбрасывается при разборе.
|
|
||||||
- Пустые строки пропускаются. Строки, начинающиеся с **`#`** или **`;`**, считаются комментариями.
|
|
||||||
- Строка с **`:`**: берётся **первая** двоеточие — всё слева (после обрезки пробелов) определяет тип правила, всё справа — значение. Если справа пусто, строка игнорируется.
|
|
||||||
- Строка **без** **`:`**: целиком трактуется как правило типа «любое совпадение» (см. ниже).
|
|
||||||
|
|
||||||
### Типы правил (левая часть до первого `:`)
|
|
||||||
|
|
||||||
| Левая часть (фрагменты совпадают как regex, без учёта регистра) | Поле события |
|
|
||||||
| --- | --- |
|
|
||||||
| `рабоч`, `workstation`, `wks` | имя рабочей станции (**WorkstationName** и аналоги в XML события) |
|
|
||||||
| `польз`, `username`, `subject`, `account`, `target user`, целое слово `user` | имя пользователя (**TargetUserName** и др.) |
|
|
||||||
| `ip`, `ip адрес`, `ipaddress`, `адрес ip` | IP источника (**IpAddress** и др.), только если в событии есть непустой IP |
|
|
||||||
|
|
||||||
Если левая часть **не** подошла ни к одному типу, но двоеточие есть — используется режим как в разборе строк Telegram: тип **«любое»**, значение — **только правая часть** (метка слева отбрасывается).
|
|
||||||
|
|
||||||
### Совпадение для типа «любое» (строка без `:` или «неизвестная» метка слева от `:`)
|
|
||||||
|
|
||||||
Проверка по очереди:
|
|
||||||
|
|
||||||
1. Если значение похоже на **IPv4** — сравнивается с IP источника в событии (точное совпадение, без учёта регистра для текста не применимо).
|
|
||||||
2. Если значение содержит **`\`** — сравнивается с **учётной записью**: полное совпадение с `DOMAIN\user` **или** совпадение с **SAM** после последнего `\` (как `DOMAIN\IVANOV` при правиле `IVANOV`).
|
|
||||||
3. Иначе сначала полное совпадение с **именем рабочей станции**, затем с **учётной записью** по тем же правилам, что в п.2.
|
|
||||||
|
|
||||||
Для явных типов **User** / **Workstation** / **Ip** используется только соответствующее поле (для пользователя — те же правила полного имени и SAM, что в п.2).
|
|
||||||
|
|
||||||
### Примеры и поставка
|
|
||||||
|
|
||||||
- Расширенные примеры строк — в **`ignore.lst.example`** в корне репозитория (скопируйте на сервер как **`ignore.lst`** и отредактируйте).
|
|
||||||
- **`Deploy-LoginMonitor.ps1`** этот файл **не копирует**: правила обычно разные на каждой машине; при необходимости создайте `ignore.lst` вручную или через вашу систему конфигурации.
|
|
||||||
|
|
||||||
## Ключевые слова (для поиска репозитория)
|
|
||||||
|
|
||||||
`rdp`, `rd-gateway`, `rdp-gateway`, `rds`, `remote-desktop`, `windows-security-log`, `eventlog`, `event-id-4624`, `event-id-4625`, `event-id-302`, `event-id-303`, `powershell`, `telegram-bot`, `watchdog`, `gpo`, `netlogon`, `domain-deployment`, `windows-server`, `monitoring`
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
-128
@@ -1,128 +0,0 @@
|
|||||||
# RDP Login Monitor
|
|
||||||
|
|
||||||
PowerShell toolkit for monitoring Windows logons with Telegram notifications.
|
|
||||||
|
|
||||||
## Recommended layout
|
|
||||||
|
|
||||||
- Installation root: **`C:\ProgramData\RDP-login-monitor\`**.
|
|
||||||
- Main script: **`Login_Monitor.ps1`** — Security log **`4624`/`4625`** (behavior depends on OS type: workstation vs server/domain controller), optional **Remote Connection Manager `1149`** when the log is available (often useful for RDP-enabled workstations), **RD Gateway** events **`302`/`303`** when the gateway role/log is present, **daily report** to Telegram (active sessions via `quser`), **heartbeat**, **log rotation**, Telegram alerts.
|
|
||||||
- Scheduled tasks: run **`Login_Monitor.ps1 -InstallTasks`** to register:
|
|
||||||
- `RDP-Login-Monitor` (main monitor),
|
|
||||||
- `RDP-Login-Monitor-Watchdog` (process health check every 5 minutes).
|
|
||||||
- Domain delivery and upgrades: **`Deploy-LoginMonitor.ps1`** + **`version.txt`** on a share such as `NETLOGON`. After a successful deploy, the startup Telegram message may include an update note (file **`deploy_last_update.txt`** next to logs).
|
|
||||||
- Full deploy/GPO guidance: **[DEPLOY.md](DEPLOY.md)**.
|
|
||||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — optional helper to prepare DPAPI-protected Base64 for the bot token / chat id.
|
|
||||||
|
|
||||||
## Notable behavior
|
|
||||||
|
|
||||||
- **`.ps1` encoding**: `.editorconfig` and `.gitattributes` encourage **`*.ps1`** as **UTF-8 with BOM** and **CRLF**, reducing mojibake and PowerShell parse issues.
|
|
||||||
- **Log encoding**: `login_monitor.log` / `watchdog.log` are written as **UTF-8 with BOM** (BOM is applied to existing files if missing) so viewers like **FAR Manager** do not mis-detect encoding.
|
|
||||||
- **`auditpol` on Russian Windows**: auditing checks use the **`Вход/выход`** category and **`Вход в систему` / `Выход из системы`** subcategories (expect **`Успех и сбой`**), avoiding errors such as `0x00000057` when English names like `Logon` are absent on a localized OS.
|
|
||||||
- **Stability**: `auditpol` is invoked via `cmd.exe` with merged stdout/stderr so `$ErrorActionPreference = 'Stop'` does not abort on stderr-only output.
|
|
||||||
|
|
||||||
## 1) Preparation
|
|
||||||
|
|
||||||
1. Create the install folder:
|
|
||||||
- `C:\ProgramData\RDP-login-monitor\`
|
|
||||||
2. Copy at least:
|
|
||||||
- `Login_Monitor.ps1`
|
|
||||||
- (for domain rollout on a share) `Deploy-LoginMonitor.ps1` and `version.txt`.
|
|
||||||
3. Edit `Login_Monitor.ps1` and set the bot token / chat:
|
|
||||||
- `$TelegramBotToken` or `$TelegramBotTokenProtectedB64`
|
|
||||||
- `$TelegramChatID` or `$TelegramChatIDProtectedB64`
|
|
||||||
4. Run elevated (Security log access and task registration).
|
|
||||||
5. Logs and auxiliary files:
|
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
|
||||||
6. (Optional) Suppress some Security alerts via **`ignore.lst`** — see **section 7** below.
|
|
||||||
|
|
||||||
## 2) Manual run
|
|
||||||
|
|
||||||
Use this to validate startup/logic without registering scheduled tasks.
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1"
|
|
||||||
```
|
|
||||||
|
|
||||||
The monitor keeps running in the session until you stop it (for example `Ctrl+C`).
|
|
||||||
|
|
||||||
## 3) Task Scheduler
|
|
||||||
|
|
||||||
You do not need to create tasks manually in the GUI.
|
|
||||||
|
|
||||||
Run:
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "C:\ProgramData\RDP-login-monitor\Login_Monitor.ps1" -InstallTasks
|
|
||||||
```
|
|
||||||
|
|
||||||
The script registers `RDP-Login-Monitor` and `RDP-Login-Monitor-Watchdog`, then triggers an immediate first run.
|
|
||||||
|
|
||||||
For domain deployment from a share you do not configure the scheduler on clients by hand — use `Deploy-LoginMonitor.ps1` (see `DEPLOY.md`).
|
|
||||||
|
|
||||||
## 4) Post-install checks
|
|
||||||
|
|
||||||
- Logs:
|
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\login_monitor.log`
|
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\watchdog.log`
|
|
||||||
- Heartbeat:
|
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\last_heartbeat.txt` updates on **`$HeartbeatInterval`** (hourly by default).
|
|
||||||
- Daily report: after the first daily window (default **09:00**, controlled by **`$DailyReportHour`** / **`$DailyReportMinute`** in `Login_Monitor.ps1`), Telegram receives a `quser` summary; last run marker: `Logs\last_daily_report.txt`.
|
|
||||||
- Startup Telegram message: with **RD Session Host** (or broader RDS session components, not gateway-only) you get the RDS/RDP session-host line; when the **RD Gateway** log is available you get a separate line about connections to **internal targets** through the gateway (302/303). A gateway-only node does not duplicate the “session host” wording.
|
|
||||||
|
|
||||||
## 5) Automatic restart on failure
|
|
||||||
|
|
||||||
`-Watchdog` inside `Login_Monitor.ps1`:
|
|
||||||
|
|
||||||
- looks for `powershell.exe` / `pwsh.exe` whose command line references `Login_Monitor.ps1`;
|
|
||||||
- if the main monitor is missing, starts it;
|
|
||||||
- if the monitor is already running, does not spawn a second instance.
|
|
||||||
|
|
||||||
## 6) Extra parameters and other repo files
|
|
||||||
|
|
||||||
- **`-SkipScheduledTaskMaintenance`**: during normal monitor startup, skip verification/recreation of scheduled tasks (if you manage tasks only via **`-InstallTasks`** or manually).
|
|
||||||
- **`Install-DeployScheduledTask.ps1`** — helper to run **`Deploy-LoginMonitor.ps1`** from a share on a schedule (see **[DEPLOY.md](DEPLOY.md)**).
|
|
||||||
- **`Watchdog_RDP_Monitor.ps1`** and **`Install-ScheduledTasks.ps1`** — **alternate** layout with a separate watchdog script and default paths under **`D:\Soft`**. For new installs, prefer the built-in **`-Watchdog`** in **`Login_Monitor.ps1`** and tasks **`RDP-Login-Monitor`** / **`RDP-Login-Monitor-Watchdog`**.
|
|
||||||
- **`ignore.lst.example`** in the repo is a template for **`ignore.lst`** to suppress selected Security notifications (see section 7).
|
|
||||||
|
|
||||||
## 7) Suppressing Security alerts: `ignore.lst`
|
|
||||||
|
|
||||||
Place a file **`C:\ProgramData\RDP-login-monitor\ignore.lst`** next to **`Login_Monitor.ps1`**. Rules in this list are evaluated **only** for Telegram notifications from Security events **`4624`/`4625`** (successful/failed logons). Built-in exclusions in the script (`ExcludedUsers`, loopback IPs, service-style accounts, etc.) still apply to all event paths; **`ignore.lst`** adds **extra** matches **for 4624/4625 only**.
|
|
||||||
|
|
||||||
**RD Gateway (`302`/`303`)**, **RCM `1149`**, the daily report, and heartbeat **are not controlled** by this file (for `1149` the list is not applied, even though a shared filter function runs).
|
|
||||||
|
|
||||||
### How the file is loaded
|
|
||||||
|
|
||||||
- The file is read during event processing; entries are **cached in memory**. If **`LastWriteTimeUtc`** changes (save after edit), the list is **reloaded automatically** — no monitor restart required.
|
|
||||||
- Encoding: **UTF-8** (`Get-Content -Encoding UTF8`). A UTF-8 BOM at the start of a line is stripped when parsing.
|
|
||||||
- Blank lines are skipped. Lines starting with **`#`** or **`;`** are comments.
|
|
||||||
- If the line contains **`:`**, the **first** colon splits the line: the left part (trimmed) selects the rule kind, the right part is the value. If the right part is empty, the line is ignored.
|
|
||||||
- If there is **no** colon, the whole trimmed line is one “match-any” value (see below).
|
|
||||||
|
|
||||||
### Rule kinds (left part before the first `:`)
|
|
||||||
|
|
||||||
| Left part (case-insensitive regex fragments) | Event field |
|
|
||||||
| --- | --- |
|
|
||||||
| `рабоч`, `workstation`, `wks` | workstation name (**WorkstationName** and similar XML fields) |
|
|
||||||
| `польз`, `username`, `subject`, `account`, `target user`, whole word `user` | user name (**TargetUserName**, etc.) |
|
|
||||||
| `ip`, `ip адрес`, `ipaddress`, `адрес ip` | source IP (**IpAddress**, etc.) when present |
|
|
||||||
|
|
||||||
If the left part matches none of the above but a colon exists, the parser behaves like the Telegram line splitter: kind is **Any**, value is **only the right part** (the label on the left is discarded).
|
|
||||||
|
|
||||||
### “Any” matching (no colon, or unknown label before `:`)
|
|
||||||
|
|
||||||
Evaluated in order:
|
|
||||||
|
|
||||||
1. If the value looks like **IPv4**, it is compared to the event source IP (exact match).
|
|
||||||
2. If the value contains **`\`**, it is matched against the **account**: full `DOMAIN\user` equality **or** SAM after the last `\` (e.g. rule `IVANOV` matches `DOMAIN\IVANOV`).
|
|
||||||
3. Otherwise: exact match against **workstation name** first, then the same account rules as in step 2.
|
|
||||||
|
|
||||||
Explicit **User** / **Workstation** / **Ip** kinds only compare their respective field (user rules use the same full-name and SAM behavior as step 2).
|
|
||||||
|
|
||||||
### Examples and deployment
|
|
||||||
|
|
||||||
- See **`ignore.lst.example`** in the repo; copy it to the server as **`ignore.lst`** and edit.
|
|
||||||
- **`Deploy-LoginMonitor.ps1`** does **not** copy this file — rules are usually host-specific; create **`ignore.lst`** manually or via your configuration tooling.
|
|
||||||
|
|
||||||
## Keywords (for discovery)
|
|
||||||
|
|
||||||
`rdp`, `rd-gateway`, `rdp-gateway`, `rds`, `remote-desktop`, `windows-security-log`, `eventlog`, `event-id-4624`, `event-id-4625`, `event-id-302`, `event-id-303`, `powershell`, `telegram-bot`, `watchdog`, `gpo`, `netlogon`, `domain-deployment`, `windows-server`, `monitoring`
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
# Файл ignore.lst (UTF-8): положите рядом с Login_Monitor.ps1 в каталоге установки:
|
|
||||||
# C:\ProgramData\RDP-login-monitor\ignore.lst
|
|
||||||
#
|
|
||||||
# Каждая непустая строка — одно правило. Строки с # или ; в начале — комментарии.
|
|
||||||
# Подавляет только уведомления Security 4624/4625 (не RD Gateway, не RCM 1149).
|
|
||||||
#
|
|
||||||
# Форматы:
|
|
||||||
# user:domain\user
|
|
||||||
# user:user
|
|
||||||
# workstation:IVANOV
|
|
||||||
# ip:111.222.333.444
|
|
||||||
# Можно вставить «как в Telegram» (берётся значение после первого «:»):
|
|
||||||
# 👤 Пользователь: user
|
|
||||||
# 🖥️ Рабочая станция (клиент из события): IVANOV
|
|
||||||
# 🌐 IP адрес: 111.222.333.444
|
|
||||||
#
|
|
||||||
# Строка без префикса:
|
|
||||||
# IVANOV — совпадение с именем рабочей станции ИЛИ с пользователем (sam) ИЛИ с IP (если строка — IPv4)
|
|
||||||
# 111.222.333.444 — только IP (в реальной конфигурации укажите действительный IPv4 клиента)
|
|
||||||
# domain\user — пользователь целиком
|
|
||||||
|
|
||||||
# user:domain\user
|
|
||||||
# workstation:IVANOV
|
|
||||||
# ip:111.222.333.444
|
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.4.3
|
1.3.12
|
||||||
|
|||||||
Reference in New Issue
Block a user