feat: monitor RDS Shadow Control and WinRM inbound (warning alerts, 1.2.23-SAC)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+365
-2
@@ -80,7 +80,7 @@ $script:MonitorLoopInitialized = $false
|
|||||||
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
# строки ниже, если правки «мелкие» и вы не хотите менять отображаемую версию в логах).
|
||||||
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
# Рекомендация: при значимых релизах меняйте и $ScriptVersion, и version.txt одинаково; при только
|
||||||
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
# исправлениях на шаре — достаточно поднять patch в version.txt (например 1.3.0.1).
|
||||||
$ScriptVersion = "1.2.22-SAC"
|
$ScriptVersion = "1.2.23-SAC"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -120,6 +120,17 @@ $RDGatewayEvents = @(302, 303)
|
|||||||
$RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational"
|
$RcmLogName = "Microsoft-Windows-TerminalServices-RemoteConnectionManager/Operational"
|
||||||
$RcmEventId = 1149
|
$RcmEventId = 1149
|
||||||
|
|
||||||
|
# RDS Shadow Control (RCM/Operational): опасные теневые подключения с управлением
|
||||||
|
$EnableRcmShadowControlMonitoring = 1
|
||||||
|
$RcmShadowControlEventIds = @(20506, 20507, 20510)
|
||||||
|
|
||||||
|
# WinRM / Enter-PSSession inbound (удалённая PowerShell-сессия на этот хост)
|
||||||
|
$EnableWinRmInboundMonitoring = 1
|
||||||
|
$WinRmLogName = 'Microsoft-Windows-WinRM/Operational'
|
||||||
|
$WinRmInboundShellEventIds = @(91)
|
||||||
|
$WinRmCorrelateSecurity4624 = 1
|
||||||
|
$WinRm4624CorrelationWindowSeconds = 15
|
||||||
|
|
||||||
$ExcludedProcesses = @(
|
$ExcludedProcesses = @(
|
||||||
"HTTP", "HTTP/*", "W3WP.EXE", "MSExchange", "SYSTEM", "LOCAL SERVICE",
|
"HTTP", "HTTP/*", "W3WP.EXE", "MSExchange", "SYSTEM", "LOCAL SERVICE",
|
||||||
"NETWORK SERVICE", "OUTLOOK.EXE", "EXCHANGE", "EDGETRANSPORT", "STORE.EXE",
|
"NETWORK SERVICE", "OUTLOOK.EXE", "EXCHANGE", "EDGETRANSPORT", "STORE.EXE",
|
||||||
@@ -1248,6 +1259,236 @@ function Format-Rcm1149Event {
|
|||||||
return $message
|
return $message
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Test-MonitorFeatureEnabled {
|
||||||
|
param(
|
||||||
|
$Value,
|
||||||
|
[bool]$DefaultEnabled = $true
|
||||||
|
)
|
||||||
|
if ($null -eq $Value) { return $DefaultEnabled }
|
||||||
|
if ($Value -is [bool]) { return $Value }
|
||||||
|
if ($Value -is [int] -or $Value -is [long]) { return ([int]$Value -ne 0) }
|
||||||
|
$s = ([string]$Value).Trim().ToLowerInvariant()
|
||||||
|
if ($s -in @('0', 'false', 'no', 'off')) { return $false }
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-WinRmLogAvailable {
|
||||||
|
try {
|
||||||
|
$logExists = Get-WinEvent -ListLog $WinRmLogName -ErrorAction SilentlyContinue
|
||||||
|
return [bool]$logExists
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-RcmShadowEventInfo {
|
||||||
|
param($Event)
|
||||||
|
$eventData = @{
|
||||||
|
TimeCreated = $Event.TimeCreated
|
||||||
|
EventId = [int]$Event.Id
|
||||||
|
ShadowerUser = '-'
|
||||||
|
TargetUser = '-'
|
||||||
|
SessionId = '-'
|
||||||
|
ShadowAction = 'control'
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$map = Get-EventDataMap -Event $Event
|
||||||
|
$eventData.ShadowerUser = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||||
|
'Shadower', 'ShadowerUser', 'Admin', 'Administrator', 'UserName', 'SubjectUserName', 'Param1'
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($eventData.ShadowerUser)) { $eventData.ShadowerUser = '-' }
|
||||||
|
$eventData.TargetUser = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||||
|
'TargetUser', 'User', 'AccountName', 'TargetUserName', 'ConnectionUser', 'Param2'
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($eventData.TargetUser)) { $eventData.TargetUser = '-' }
|
||||||
|
$sid = Get-FirstNonEmptyMapValue -DataMap $map -Keys @('SessionID', 'SessionId', 'Session', 'Param3')
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($sid)) { $eventData.SessionId = [string]$sid }
|
||||||
|
|
||||||
|
if ($eventData.ShadowerUser -eq '-' -and $Event.Properties.Count -gt 0) {
|
||||||
|
$eventData.ShadowerUser = [string]$Event.Properties[0].Value
|
||||||
|
}
|
||||||
|
if ($eventData.TargetUser -eq '-' -and $Event.Properties.Count -gt 1) {
|
||||||
|
$eventData.TargetUser = [string]$Event.Properties[1].Value
|
||||||
|
}
|
||||||
|
if ($eventData.SessionId -eq '-' -and $Event.Properties.Count -gt 2) {
|
||||||
|
$eventData.SessionId = [string]$Event.Properties[2].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = [string]$Event.Message
|
||||||
|
if ($eventData.SessionId -eq '-' -and $msg -match '(?i)(?:session|сеанс)\s*(?:id\s*)?[:\s#]*(\d+)') {
|
||||||
|
$eventData.SessionId = $Matches[1]
|
||||||
|
}
|
||||||
|
switch ([int]$Event.Id) {
|
||||||
|
20506 { $eventData.ShadowAction = 'control_started' }
|
||||||
|
20507 { $eventData.ShadowAction = 'control_stopped' }
|
||||||
|
20510 { $eventData.ShadowAction = 'control_permission' }
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Log "Ошибка разбора RCM shadow $($Event.Id): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
return $eventData
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-RcmShadowControlEvent {
|
||||||
|
param(
|
||||||
|
[hashtable]$Info,
|
||||||
|
[string]$SecurityLogComputerName
|
||||||
|
)
|
||||||
|
$logHost = $SecurityLogComputerName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($logHost)) { $logHost = $env:COMPUTERNAME }
|
||||||
|
$hHost = (ConvertTo-TelegramHtml (Get-MonitorServerLabelWithIp))
|
||||||
|
$hLog = (ConvertTo-TelegramHtml $logHost)
|
||||||
|
$hShadower = (ConvertTo-TelegramHtml $Info.ShadowerUser)
|
||||||
|
$hTarget = (ConvertTo-TelegramHtml $Info.TargetUser)
|
||||||
|
$hSid = (ConvertTo-TelegramHtml $Info.SessionId)
|
||||||
|
$hTime = (ConvertTo-TelegramHtml ($Info.TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')))
|
||||||
|
|
||||||
|
$header = switch ($Info.ShadowAction) {
|
||||||
|
'control_started' { '🎭 RDS SHADOW CONTROL — начато' }
|
||||||
|
'control_stopped' { '🎭 RDS SHADOW CONTROL — остановлено' }
|
||||||
|
'control_permission' { '🎭 RDS SHADOW CONTROL — разрешение выдано' }
|
||||||
|
default { '🎭 RDS SHADOW CONTROL' }
|
||||||
|
}
|
||||||
|
$message = "<b>$header</b>`r`n"
|
||||||
|
$message += "🏢 Сервер: $hHost`r`n"
|
||||||
|
$message += "👤 Администратор (shadow): $hShadower`r`n"
|
||||||
|
$message += "🎯 Сессия пользователя: $hTarget`r`n"
|
||||||
|
if ($Info.SessionId -ne '-') {
|
||||||
|
$message += "🔢 Session ID: $hSid`r`n"
|
||||||
|
}
|
||||||
|
$message += "🕐 Время: $hTime`r`n"
|
||||||
|
$message += "🔢 Event ID: $($Info.EventId) (RemoteConnectionManager)"
|
||||||
|
return $message
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacTypeForRcmShadowEvent {
|
||||||
|
param([int]$EventId)
|
||||||
|
switch ($EventId) {
|
||||||
|
20506 { return 'rdp.shadow.control.started' }
|
||||||
|
20507 { return 'rdp.shadow.control.stopped' }
|
||||||
|
20510 { return 'rdp.shadow.control.permission' }
|
||||||
|
default { return 'rdp.shadow.control.started' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-WinRm91EventInfo {
|
||||||
|
param($Event)
|
||||||
|
$eventData = @{
|
||||||
|
TimeCreated = $Event.TimeCreated
|
||||||
|
EventId = [int]$Event.Id
|
||||||
|
User = '-'
|
||||||
|
ResourceUri = '-'
|
||||||
|
SourceIP = '-'
|
||||||
|
LogonType = 0
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
$map = Get-EventDataMap -Event $Event
|
||||||
|
$eventData.User = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||||
|
'user', 'User', 'UserName', 'AccountName', 'SubjectUserName'
|
||||||
|
)
|
||||||
|
$eventData.ResourceUri = Get-FirstNonEmptyMapValue -DataMap $map -Keys @(
|
||||||
|
'resourceUri', 'ResourceUri', 'shellId', 'ShellId', 'connection', 'Connection'
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($eventData.User) -and $Event.Properties.Count -gt 0) {
|
||||||
|
$eventData.User = [string]$Event.Properties[0].Value
|
||||||
|
}
|
||||||
|
$msg = [string]$Event.Message
|
||||||
|
if ($eventData.ResourceUri -eq '-' -and $msg -match '(?i)ResourceUri:\s*(.+)$') {
|
||||||
|
$eventData.ResourceUri = $Matches[1].Trim()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
Write-Log "Ошибка разбора WinRM $($Event.Id): $($_.Exception.Message)"
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($eventData.User)) { $eventData.User = '-' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($eventData.ResourceUri)) { $eventData.ResourceUri = '-' }
|
||||||
|
return $eventData
|
||||||
|
}
|
||||||
|
|
||||||
|
function Find-CorrelatedNetworkLogon4624 {
|
||||||
|
param(
|
||||||
|
[datetime]$AroundTime,
|
||||||
|
[string]$UsernameHint = '',
|
||||||
|
[int]$WindowSeconds = 15
|
||||||
|
)
|
||||||
|
$start = $AroundTime.AddSeconds(-1 * [Math]::Abs($WindowSeconds))
|
||||||
|
$end = $AroundTime.AddSeconds([Math]::Abs($WindowSeconds))
|
||||||
|
try {
|
||||||
|
$events = @(Get-WinEvent -FilterHashtable @{
|
||||||
|
LogName = 'Security'
|
||||||
|
ID = 4624
|
||||||
|
StartTime = $start
|
||||||
|
} -ErrorAction SilentlyContinue | Where-Object { $_.TimeCreated -le $end })
|
||||||
|
} catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
$best = $null
|
||||||
|
foreach ($ev in $events) {
|
||||||
|
$info = Get-LoginEventInfo -Event $ev
|
||||||
|
if ($info.LogonType -ne 3) { continue }
|
||||||
|
if ($info.SourceIP -eq '-' -or [string]::IsNullOrWhiteSpace($info.SourceIP)) { continue }
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($UsernameHint) -and $UsernameHint -ne '-') {
|
||||||
|
if (-not (Test-RdpMonitorUsernameMatchesToken -Username $info.Username -Token $UsernameHint)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($null -eq $best -or $ev.TimeCreated -gt $best.TimeCreated) {
|
||||||
|
$best = [pscustomobject]@{
|
||||||
|
Username = $info.Username
|
||||||
|
SourceIP = $info.SourceIP
|
||||||
|
LogonType = $info.LogonType
|
||||||
|
ProcessName = $info.ProcessName
|
||||||
|
TimeCreated = $ev.TimeCreated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $best
|
||||||
|
}
|
||||||
|
|
||||||
|
function Format-WinRmSessionEvent {
|
||||||
|
param(
|
||||||
|
[hashtable]$Info,
|
||||||
|
[string]$SecurityLogComputerName
|
||||||
|
)
|
||||||
|
$logHost = $SecurityLogComputerName
|
||||||
|
if ([string]::IsNullOrWhiteSpace($logHost)) { $logHost = $env:COMPUTERNAME }
|
||||||
|
$hHost = (ConvertTo-TelegramHtml (Get-MonitorServerLabelWithIp))
|
||||||
|
$hUser = (ConvertTo-TelegramHtml $Info.User)
|
||||||
|
$hIp = (ConvertTo-TelegramHtml $Info.SourceIP)
|
||||||
|
$hUri = (ConvertTo-TelegramHtml $Info.ResourceUri)
|
||||||
|
$hTime = (ConvertTo-TelegramHtml ($Info.TimeCreated.ToString('dd.MM.yyyy HH:mm:ss')))
|
||||||
|
|
||||||
|
$message = "<b>⚠️ WinRM / Enter-PSSession — удалённая shell</b>`r`n"
|
||||||
|
$message += "🏢 Сервер: $hHost`r`n"
|
||||||
|
$message += "👤 Пользователь: $hUser`r`n"
|
||||||
|
if ($Info.SourceIP -ne '-') {
|
||||||
|
$message += "🌐 IP источника: $hIp`r`n"
|
||||||
|
}
|
||||||
|
if ($Info.ResourceUri -ne '-') {
|
||||||
|
$message += "🔗 ResourceUri: $hUri`r`n"
|
||||||
|
}
|
||||||
|
$message += "🕐 Время: $hTime`r`n"
|
||||||
|
$message += "🔢 Event ID: $($Info.EventId) (WinRM Operational)"
|
||||||
|
return $message
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-RdpMonitorNotifyDedup {
|
||||||
|
param(
|
||||||
|
[string]$Key,
|
||||||
|
[int]$WindowSeconds = 90
|
||||||
|
)
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Key)) { return $false }
|
||||||
|
if (-not $script:NotifyDedupCache) {
|
||||||
|
$script:NotifyDedupCache = @{}
|
||||||
|
}
|
||||||
|
$now = Get-Date
|
||||||
|
if ($script:NotifyDedupCache.ContainsKey($Key)) {
|
||||||
|
$until = $script:NotifyDedupCache[$Key]
|
||||||
|
if ($now -lt $until) { return $true }
|
||||||
|
}
|
||||||
|
$script:NotifyDedupCache[$Key] = $now.AddSeconds($WindowSeconds)
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
function Get-MonitorServerLabel {
|
function Get-MonitorServerLabel {
|
||||||
$label = $null
|
$label = $null
|
||||||
if (Get-Variable -Name ServerDisplayName -ErrorAction SilentlyContinue) {
|
if (Get-Variable -Name ServerDisplayName -ErrorAction SilentlyContinue) {
|
||||||
@@ -1757,8 +1998,14 @@ function Parse-RdpMonitorIgnoreListLine {
|
|||||||
if ($line -match '^(?i)(4740|lockout|блокир)\s*:\s*(.+)$') {
|
if ($line -match '^(?i)(4740|lockout|блокир)\s*:\s*(.+)$') {
|
||||||
$scopes = @('4740')
|
$scopes = @('4740')
|
||||||
$line = $Matches[2].Trim()
|
$line = $Matches[2].Trim()
|
||||||
|
} elseif ($line -match '^(?i)(shadow|20506)\s*:\s*(.+)$') {
|
||||||
|
$scopes = @('20506', '20507', '20510')
|
||||||
|
$line = $Matches[2].Trim()
|
||||||
|
} elseif ($line -match '^(?i)(winrm|pssession|91)\s*:\s*(.+)$') {
|
||||||
|
$scopes = @('winrm')
|
||||||
|
$line = $Matches[2].Trim()
|
||||||
} elseif ($line -match '^(?i)(all|\*)\s*:\s*(.+)$') {
|
} elseif ($line -match '^(?i)(all|\*)\s*:\s*(.+)$') {
|
||||||
$scopes = @('4624', '4625', '4740')
|
$scopes = @('4624', '4625', '4740', '20506', '20507', '20510', 'winrm')
|
||||||
$line = $Matches[2].Trim()
|
$line = $Matches[2].Trim()
|
||||||
}
|
}
|
||||||
if ([string]::IsNullOrWhiteSpace($line)) { return $null }
|
if ([string]::IsNullOrWhiteSpace($line)) { return $null }
|
||||||
@@ -2756,6 +3003,21 @@ function Start-LoginMonitor {
|
|||||||
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
|
Write-Log "Мониторинг 4740 задан для КД '$LockoutMonitorDomainController', но этот узел — $env:COMPUTERNAME (блокировки не отслеживаются)."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (Test-MonitorFeatureEnabled -Value $EnableRcmShadowControlMonitoring) {
|
||||||
|
if (Test-RcmLogAvailable) {
|
||||||
|
Write-Log "RDS Shadow Control: включён (RCM Operational IDs: $($RcmShadowControlEventIds -join ', '))."
|
||||||
|
} else {
|
||||||
|
Write-Log "RDS Shadow Control: включён в настройках, но журнал RCM недоступен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Test-MonitorFeatureEnabled -Value $EnableWinRmInboundMonitoring) {
|
||||||
|
if (Test-WinRmLogAvailable) {
|
||||||
|
Write-Log "WinRM inbound (Enter-PSSession): включён (Operational IDs: $($WinRmInboundShellEventIds -join ', '); correlate 4624=$WinRmCorrelateSecurity4624)."
|
||||||
|
} else {
|
||||||
|
Write-Log "WinRM inbound: включён в настройках, но журнал WinRM Operational недоступен."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$script:MonitorStartedAt = Get-Date
|
$script:MonitorStartedAt = Get-Date
|
||||||
$script:HeartbeatStaleAlertActive = $false
|
$script:HeartbeatStaleAlertActive = $false
|
||||||
|
|
||||||
@@ -2777,13 +3039,19 @@ function Start-LoginMonitor {
|
|||||||
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
|
Write-Log "Рабочая станция: журнал Remote Connection Manager недоступен — уведомления только по Security 4624/4625 (LogonType 10). Проверьте, что включён удалённый рабочий стол."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$rcmShadowMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableRcmShadowControlMonitoring) -and (Test-RcmLogAvailable)
|
||||||
|
$winRmMonitoringEnabled = (Test-MonitorFeatureEnabled -Value $EnableWinRmInboundMonitoring) -and (Test-WinRmLogAvailable)
|
||||||
|
|
||||||
$nextHeartbeatTime = (Get-Date).AddSeconds($HeartbeatInterval)
|
$nextHeartbeatTime = (Get-Date).AddSeconds($HeartbeatInterval)
|
||||||
$nextRotationCheck = Check-AndRotateLog
|
$nextRotationCheck = Check-AndRotateLog
|
||||||
$nextReportCheck = Check-AndSendDailyReport
|
$nextReportCheck = Check-AndSendDailyReport
|
||||||
$lastCheckTime = (Get-Date).AddSeconds(-10)
|
$lastCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
|
$lastGatewayCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
|
$lastRcmCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
|
$lastRcmShadowCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
|
$lastWinRmCheckTime = (Get-Date).AddSeconds(-10)
|
||||||
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
|
$lastLockout4740CheckTime = (Get-Date).AddSeconds(-10)
|
||||||
|
if (-not $script:NotifyDedupCache) { $script:NotifyDedupCache = @{} }
|
||||||
$monitorEvents = @(4624, 4625, 4648)
|
$monitorEvents = @(4624, 4625, 4648)
|
||||||
|
|
||||||
while ($true) {
|
while ($true) {
|
||||||
@@ -3013,6 +3281,101 @@ function Start-LoginMonitor {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($rcmShadowMonitoringEnabled) {
|
||||||
|
$shadowEvents = Get-WinEvent -FilterHashtable @{
|
||||||
|
LogName = $RcmLogName
|
||||||
|
ID = $RcmShadowControlEventIds
|
||||||
|
StartTime = $lastRcmShadowCheckTime
|
||||||
|
} -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($shadowEvents) {
|
||||||
|
foreach ($event in $shadowEvents) {
|
||||||
|
if ($event.TimeCreated -le $lastRcmShadowCheckTime) { continue }
|
||||||
|
$sh = Get-RcmShadowEventInfo -Event $event
|
||||||
|
if ($sh.ShadowerUser -like '*$') { continue }
|
||||||
|
if (Test-RdpMonitorIgnoreListMatch -EventId ([string]$event.Id) -Username $sh.ShadowerUser `
|
||||||
|
-ComputerName $sh.TargetUser -SourceIP '-') {
|
||||||
|
Write-Log "Skip shadow $($event.Id): Shadower=$($sh.ShadowerUser) Target=$($sh.TargetUser) — ignore.lst"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$dedupKey = "shadow|$($event.Id)|$($sh.ShadowerUser)|$($sh.TargetUser)|$($sh.SessionId)"
|
||||||
|
if (Test-RdpMonitorNotifyDedup -Key $dedupKey -WindowSeconds 120) {
|
||||||
|
Write-Log "Notify dedup shadow $($event.Id): $dedupKey"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$sacType = Get-SacTypeForRcmShadowEvent -EventId $event.Id
|
||||||
|
$msg = Format-RcmShadowControlEvent -Info $sh -SecurityLogComputerName $event.MachineName
|
||||||
|
Write-Log "Notify shadow $($event.Id): Shadower=$($sh.ShadowerUser) Target=$($sh.TargetUser) Session=$($sh.SessionId)"
|
||||||
|
Send-MonitorNotification -Message $msg `
|
||||||
|
-EmailSubject "RDP Login Monitor: RDS Shadow $($event.Id)" `
|
||||||
|
-SacEventType $sacType -SacSeverity 'warning' `
|
||||||
|
-SacTitle "RDS Shadow Control $($event.Id)" `
|
||||||
|
-SacSummary "Shadow $($event.Id) $($sh.ShadowerUser) -> $($sh.TargetUser)" `
|
||||||
|
-SacDetails @{
|
||||||
|
event_id_windows = [int]$event.Id
|
||||||
|
shadow_mode = 'control'
|
||||||
|
shadow_action = $sh.ShadowAction
|
||||||
|
shadower_user = $sh.ShadowerUser
|
||||||
|
target_user = $sh.TargetUser
|
||||||
|
target_session_id = $sh.SessionId
|
||||||
|
session_id = $sh.SessionId
|
||||||
|
} | Out-Null
|
||||||
|
}
|
||||||
|
$lastRcmShadowCheckTime = ($shadowEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($winRmMonitoringEnabled) {
|
||||||
|
$winRmEvents = Get-WinEvent -FilterHashtable @{
|
||||||
|
LogName = $WinRmLogName
|
||||||
|
ID = $WinRmInboundShellEventIds
|
||||||
|
StartTime = $lastWinRmCheckTime
|
||||||
|
} -ErrorAction SilentlyContinue
|
||||||
|
|
||||||
|
if ($winRmEvents) {
|
||||||
|
foreach ($event in $winRmEvents) {
|
||||||
|
if ($event.TimeCreated -le $lastWinRmCheckTime) { continue }
|
||||||
|
$wr = Get-WinRm91EventInfo -Event $event
|
||||||
|
if ($wr.User -like '*$') { continue }
|
||||||
|
if (Test-MonitorFeatureEnabled -Value $WinRmCorrelateSecurity4624) {
|
||||||
|
$corr = Find-CorrelatedNetworkLogon4624 -AroundTime $wr.TimeCreated `
|
||||||
|
-UsernameHint $wr.User -WindowSeconds $WinRm4624CorrelationWindowSeconds
|
||||||
|
if ($null -ne $corr) {
|
||||||
|
$wr.SourceIP = $corr.SourceIP
|
||||||
|
if ($wr.User -eq '-' -and $corr.Username -ne '-') { $wr.User = $corr.Username }
|
||||||
|
$wr.LogonType = $corr.LogonType
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Test-RdpMonitorIgnoreListMatch -EventId 'winrm' -Username $wr.User -SourceIP $wr.SourceIP) {
|
||||||
|
Write-Log "Skip WinRM $($event.Id): User=$($wr.User) IP=$($wr.SourceIP) — ignore.lst"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$dedupKey = "winrm|$($event.Id)|$($wr.User)|$($wr.SourceIP)|$($event.RecordId)"
|
||||||
|
if (Test-RdpMonitorNotifyDedup -Key $dedupKey -WindowSeconds 90) {
|
||||||
|
Write-Log "Notify dedup WinRM $($event.Id): $dedupKey"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
$msg = Format-WinRmSessionEvent -Info $wr -SecurityLogComputerName $event.MachineName
|
||||||
|
Write-Log "Notify WinRM $($event.Id): User=$($wr.User) IP=$($wr.SourceIP)"
|
||||||
|
Send-MonitorNotification -Message $msg `
|
||||||
|
-EmailSubject "RDP Login Monitor: WinRM shell (Enter-PSSession)" `
|
||||||
|
-SacEventType 'winrm.session.started' -SacSeverity 'warning' `
|
||||||
|
-SacTitle 'WinRM remote shell (Enter-PSSession)' `
|
||||||
|
-SacSummary "WinRM 91 $($wr.User) from $($wr.SourceIP)" `
|
||||||
|
-SacDetails @{
|
||||||
|
event_id_windows = [int]$event.Id
|
||||||
|
user = $wr.User
|
||||||
|
source_ip = $wr.SourceIP
|
||||||
|
ip_address = $wr.SourceIP
|
||||||
|
resource_uri = $wr.ResourceUri
|
||||||
|
logon_type = $wr.LogonType
|
||||||
|
transport = 'winrm'
|
||||||
|
} | Out-Null
|
||||||
|
}
|
||||||
|
$lastWinRmCheckTime = ($winRmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ($lockout4740Enabled) {
|
if ($lockout4740Enabled) {
|
||||||
$lockoutEvents = Get-WinEvent -FilterHashtable @{
|
$lockoutEvents = Get-WinEvent -FilterHashtable @{
|
||||||
LogName = 'Security'
|
LogName = 'Security'
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ PowerShell-набор для мониторинга входов в Windows с
|
|||||||
## Актуальная схема (рекомендуется)
|
## Актуальная схема (рекомендуется)
|
||||||
|
|
||||||
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**.
|
- Базовый путь установки: **`C:\ProgramData\RDP-login-monitor\`**.
|
||||||
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при всплеске **`4625`** — **агрегированные оповещения** (два порога: IP+пользователь и только IP), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), при роли **RD Gateway** — **`302`/`303`**, на **КД, где запущен монитор** (имя совпадает с **`$LockoutMonitorDomainController`**) — **`4740`** (блокировка УЗ + IP из IIS ActiveSync), **ежедневный отчёт** (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram и/или Email.
|
- Основной скрипт: **`Login_Monitor.ps1`** — журнал Security **`4624`/`4625`** (логика зависит от типа ОС: рабочая станция или сервер/КД), при всплеске **`4625`** — **агрегированные оповещения** (два порога: IP+пользователь и только IP), при наличии журнала — **Remote Connection Manager `1149`** (часто актуально для РС с RDP), **RDS Shadow Control** (`20506`/`20507`/`20510`, severity warning), **WinRM inbound / Enter-PSSession** (Operational `91`, severity warning), при роли **RD Gateway** — **`302`/`303`**, на **КД, где запущен монитор** (имя совпадает с **`$LockoutMonitorDomainController`**) — **`4740`** (блокировка УЗ + IP из IIS ActiveSync), **ежедневный отчёт** (активные сессии через `quser`), **heartbeat**, **ротация логов**, уведомления в Telegram и/или Email.
|
||||||
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт:
|
- Установка задач: запуск **`Login_Monitor.ps1 -InstallTasks`** создаёт:
|
||||||
- `RDP-Login-Monitor` (основной монитор),
|
- `RDP-Login-Monitor` (основной монитор),
|
||||||
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
|
- `RDP-Login-Monitor-Watchdog` (контроль процесса каждые 5 минут).
|
||||||
|
|||||||
+10
-1
@@ -6,7 +6,9 @@
|
|||||||
# Область действия (префикс в начале строки, необязателен):
|
# Область действия (префикс в начале строки, необязателен):
|
||||||
# (по умолчанию) — только Security 4624/4625
|
# (по умолчанию) — только Security 4624/4625
|
||||||
# 4740: — только блокировка учётной записи (4740); для IP — любой IP из IIS
|
# 4740: — только блокировка учётной записи (4740); для IP — любой IP из IIS
|
||||||
# all: — и 4624/4625, и 4740
|
# shadow: — RDS Shadow Control (RCM 20506/20507/20510)
|
||||||
|
# winrm: / pssession: — WinRM inbound / Enter-PSSession (Operational 91)
|
||||||
|
# all: — 4624/4625, 4740, shadow, winrm
|
||||||
#
|
#
|
||||||
# Форматы правила (после префикса области):
|
# Форматы правила (после префикса области):
|
||||||
# user:domain\user
|
# user:domain\user
|
||||||
@@ -26,5 +28,12 @@
|
|||||||
# 4740:user:test.user
|
# 4740:user:test.user
|
||||||
# 4740:ip:203.0.113.50
|
# 4740:ip:203.0.113.50
|
||||||
|
|
||||||
|
# --- только WinRM / Enter-PSSession inbound ---
|
||||||
|
# winrm:user:DOMAIN\jump-admin
|
||||||
|
# winrm:ip:192.168.160.50
|
||||||
|
|
||||||
|
# --- только RDS Shadow Control ---
|
||||||
|
# shadow:user:DOMAIN\helpdesk
|
||||||
|
|
||||||
# --- все перечисленные события ---
|
# --- все перечисленные события ---
|
||||||
# all:user:domain\noise_account
|
# all:user:domain\noise_account
|
||||||
|
|||||||
@@ -42,8 +42,13 @@ $SacApiKey = 'sac_UkOsAT3UWiQS54KK5OJPBDCSucysQDrKFju28wmYiz8'
|
|||||||
# $SacTlsSkipVerify = $false
|
# $SacTlsSkipVerify = $false
|
||||||
# $SacFallbackFailures = 5
|
# $SacFallbackFailures = 5
|
||||||
# $false = не слать report.daily.rdp с агента (суточный отчёт только из SAC)
|
# $false = не слать report.daily.rdp с агента (суточный отчёт только из SAC)
|
||||||
# Важно: только $false или 0/1 — строка "false" без $ ломает загрузку настроек!
|
# В settings.ps1 используйте 1/0 или $true/$false — не пишите голое false без $
|
||||||
$DailyReportEnabled = 1
|
$DailyReportEnabled = 1
|
||||||
|
|
||||||
|
# --- RDS Shadow Control + WinRM inbound (Enter-PSSession), severity warning ---
|
||||||
|
# $EnableRcmShadowControlMonitoring = 1 # RCM Operational 20506/20507/20510
|
||||||
|
# $EnableWinRmInboundMonitoring = 1 # WinRM Operational 91 (+ correlate Security 4624)
|
||||||
|
# $WinRmCorrelateSecurity4624 = 1
|
||||||
# Проверка: powershell -File Login_Monitor.ps1 -CheckSac
|
# Проверка: powershell -File Login_Monitor.ps1 -CheckSac
|
||||||
|
|
||||||
# --- Узкое исключение шумовых сетевых логонов (LogonType=3, Advapi) ---
|
# --- Узкое исключение шумовых сетевых логонов (LogonType=3, Advapi) ---
|
||||||
|
|||||||
+1
-2
@@ -1,2 +1 @@
|
|||||||
1.2.22-SAC
|
1.2.23-SAC
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user