feat: SAC client 1.2.0-SAC and daily report details for ingest
This commit is contained in:
@@ -238,6 +238,15 @@ try {
|
|||||||
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
|
Copy-Item -LiteralPath $sourceScript -Destination $LocalScript -Force
|
||||||
Write-DeployLog "Файл скопирован: $LocalScript"
|
Write-DeployLog "Файл скопирован: $LocalScript"
|
||||||
|
|
||||||
|
$sourceSac = Join-Path $shareRoot 'Sac-Client.ps1'
|
||||||
|
$localSac = Join-Path $InstallRoot 'Sac-Client.ps1'
|
||||||
|
if (Test-Path -LiteralPath $sourceSac) {
|
||||||
|
Copy-Item -LiteralPath $sourceSac -Destination $localSac -Force
|
||||||
|
Write-DeployLog "Файл скопирован: $localSac"
|
||||||
|
} else {
|
||||||
|
Write-DeployLog "Предупреждение: на шаре нет Sac-Client.ps1 — SAC будет недоступен до следующего деплоя."
|
||||||
|
}
|
||||||
|
|
||||||
$installArgs = @(
|
$installArgs = @(
|
||||||
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
|
'-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $LocalScript, '-InstallTasks'
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
## Копируемые файлы
|
## Копируемые файлы
|
||||||
|
|
||||||
- `Login_Monitor.ps1`
|
- `Login_Monitor.ps1`
|
||||||
|
- `Sac-Client.ps1`
|
||||||
- `version.txt`
|
- `version.txt`
|
||||||
- `Deploy-LoginMonitor.ps1`
|
- `Deploy-LoginMonitor.ps1`
|
||||||
- `Exchange-MailSecurity.ps1`
|
- `Exchange-MailSecurity.ps1`
|
||||||
|
|||||||
+176
-15
@@ -35,7 +35,8 @@ param(
|
|||||||
[string]$MailSmtpPasswordProtectedB64 = '',
|
[string]$MailSmtpPasswordProtectedB64 = '',
|
||||||
[switch]$Watchdog,
|
[switch]$Watchdog,
|
||||||
[switch]$InstallTasks,
|
[switch]$InstallTasks,
|
||||||
[switch]$SkipScheduledTaskMaintenance
|
[switch]$SkipScheduledTaskMaintenance,
|
||||||
|
[switch]$CheckSac
|
||||||
)
|
)
|
||||||
|
|
||||||
Set-StrictMode -Version Latest
|
Set-StrictMode -Version Latest
|
||||||
@@ -74,7 +75,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.5.11"
|
$ScriptVersion = "1.2.0-SAC"
|
||||||
|
|
||||||
# Логи (все под InstallRoot)
|
# Логи (все под InstallRoot)
|
||||||
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
$LogFile = Join-Path $script:InstallRoot "Logs\login_monitor.log"
|
||||||
@@ -185,6 +186,16 @@ $MailTo = ''
|
|||||||
$MailSmtpStartTls = $true
|
$MailSmtpStartTls = $true
|
||||||
$MailSmtpSsl = $false
|
$MailSmtpSsl = $false
|
||||||
|
|
||||||
|
# Security Alert Center (см. security-alert-center/docs/agent-integration.md)
|
||||||
|
$UseSAC = 'off'
|
||||||
|
$SacUrl = ''
|
||||||
|
$SacApiKey = ''
|
||||||
|
$SacSpoolDir = ''
|
||||||
|
$SacAgentIdFile = ''
|
||||||
|
$SacTimeoutSec = 12
|
||||||
|
$SacTlsSkipVerify = $false
|
||||||
|
$SacFallbackFailures = 5
|
||||||
|
|
||||||
$script:LoginMonitorSettingsFile = Join-Path $script:InstallRoot 'login_monitor.settings.ps1'
|
$script:LoginMonitorSettingsFile = Join-Path $script:InstallRoot 'login_monitor.settings.ps1'
|
||||||
$script:LoginMonitorSettingsLoaded = $false
|
$script:LoginMonitorSettingsLoaded = $false
|
||||||
if (Test-Path -LiteralPath $script:LoginMonitorSettingsFile) {
|
if (Test-Path -LiteralPath $script:LoginMonitorSettingsFile) {
|
||||||
@@ -764,12 +775,52 @@ function Test-MailSmtpConnection {
|
|||||||
return $true
|
return $true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$script:SacClientLoaded = $false
|
||||||
|
foreach ($sacPath in @(
|
||||||
|
(Join-Path $PSScriptRoot 'Sac-Client.ps1'),
|
||||||
|
(Join-Path $script:InstallRoot 'Sac-Client.ps1')
|
||||||
|
)) {
|
||||||
|
if (Test-Path -LiteralPath $sacPath) {
|
||||||
|
. $sacPath
|
||||||
|
$script:SacClientLoaded = $true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-PlainSummaryFromTelegramHtml {
|
||||||
|
param([string]$Message)
|
||||||
|
$plain = ($Message -replace '<[^>]+>', ' ' -replace '\s+', ' ').Trim()
|
||||||
|
if ($plain.Length -gt 500) { $plain = $plain.Substring(0, 500) }
|
||||||
|
return $plain
|
||||||
|
}
|
||||||
|
|
||||||
function Send-MonitorNotification {
|
function Send-MonitorNotification {
|
||||||
param(
|
param(
|
||||||
[string]$Message,
|
[string]$Message,
|
||||||
[string]$EmailSubject = "RDP Login Monitor"
|
[string]$EmailSubject = "RDP Login Monitor",
|
||||||
|
[string]$SacEventType = '',
|
||||||
|
[string]$SacSeverity = 'info',
|
||||||
|
[string]$SacTitle = '',
|
||||||
|
[string]$SacSummary = '',
|
||||||
|
[hashtable]$SacDetails = $null
|
||||||
)
|
)
|
||||||
|
|
||||||
|
$title = if (-not [string]::IsNullOrWhiteSpace($SacTitle)) { $SacTitle } else { $EmailSubject }
|
||||||
|
$summary = if (-not [string]::IsNullOrWhiteSpace($SacSummary)) {
|
||||||
|
$SacSummary
|
||||||
|
} else {
|
||||||
|
Get-PlainSummaryFromTelegramHtml -Message $Message
|
||||||
|
}
|
||||||
|
$etype = if (-not [string]::IsNullOrWhiteSpace($SacEventType)) { $SacEventType } else { 'agent.notification' }
|
||||||
|
|
||||||
|
if ($script:SacClientLoaded -and (Get-Command Send-NotifyOrSac -ErrorAction SilentlyContinue)) {
|
||||||
|
$sacMode = Get-SacNormalizedMode
|
||||||
|
if ($sacMode -ne 'off') {
|
||||||
|
return Send-NotifyOrSac -EventType $etype -Severity $SacSeverity -Title $title -Summary $summary `
|
||||||
|
-TelegramMessage $Message -EmailSubject $EmailSubject -Details $SacDetails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$channels = @(Get-NotifyOrderChannels)
|
$channels = @(Get-NotifyOrderChannels)
|
||||||
if ($channels.Count -eq 0) {
|
if ($channels.Count -eq 0) {
|
||||||
Write-Log "Оповещение не отправлено: нет настроенных каналов (Telegram и/или SMTP)"
|
Write-Log "Оповещение не отправлено: нет настроенных каналов (Telegram и/или SMTP)"
|
||||||
@@ -805,6 +856,15 @@ if ($script:LoginMonitorSettingsLoaded) {
|
|||||||
Write-Log "Предупреждение: login_monitor.settings.ps1 не найден — Telegram/SMTP/4740 не настроены (скопируйте login_monitor.settings.example.ps1)."
|
Write-Log "Предупреждение: login_monitor.settings.ps1 не найден — Telegram/SMTP/4740 не настроены (скопируйте login_monitor.settings.example.ps1)."
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($CheckSac) {
|
||||||
|
if (-not $script:SacClientLoaded) {
|
||||||
|
Write-Log "ОШИБКА: Sac-Client.ps1 не найден рядом с Login_Monitor.ps1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$code = Test-SacConnection
|
||||||
|
exit $code
|
||||||
|
}
|
||||||
|
|
||||||
Invoke-RdpMonitorProcessMigrationAndRelaunch
|
Invoke-RdpMonitorProcessMigrationAndRelaunch
|
||||||
Lock-RdpMonitorSingleInstance
|
Lock-RdpMonitorSingleInstance
|
||||||
Ensure-RdpMonitorScheduledTasks
|
Ensure-RdpMonitorScheduledTasks
|
||||||
@@ -1219,11 +1279,22 @@ function Send-Heartbeat {
|
|||||||
$message += " IP из IIS не заданы (<code>ExchangeIisLogPath</code> пуст)."
|
$message += " IP из IIS не заданы (<code>ExchangeIisLogPath</code> пуст)."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: запуск" | Out-Null
|
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: запуск" `
|
||||||
|
-SacEventType 'agent.lifecycle' -SacSeverity 'info' `
|
||||||
|
-SacTitle 'RDP login monitor started' `
|
||||||
|
-SacSummary "Мониторинг запущен на $env:COMPUTERNAME, версия $ScriptVersion" `
|
||||||
|
-SacDetails @{ lifecycle = 'started' } | Out-Null
|
||||||
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
Write-Log "Отправлено уведомление о запуске скрипта (каналы: $notifyChain)"
|
||||||
} else {
|
} else {
|
||||||
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
|
Write-TextFileUtf8Bom -Path $HeartbeatFile -Text $timestamp
|
||||||
$script:HeartbeatStaleAlertActive = $false
|
$script:HeartbeatStaleAlertActive = $false
|
||||||
|
if ($script:SacClientLoaded -and (Get-SacNormalizedMode) -ne 'off') {
|
||||||
|
Send-MonitorNotification -Message '' -EmailSubject 'RDP Login Monitor: heartbeat' `
|
||||||
|
-SacEventType 'agent.heartbeat' -SacSeverity 'info' `
|
||||||
|
-SacTitle 'RDP monitor heartbeat' `
|
||||||
|
-SacSummary "Heartbeat $env:COMPUTERNAME $timestamp" `
|
||||||
|
-SacDetails @{ host = $env:COMPUTERNAME } | Out-Null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1269,7 +1340,10 @@ function Test-AndSendHeartbeatStaleAlert {
|
|||||||
$msg += "📄 Последний heartbeat: $hLast`r`n"
|
$msg += "📄 Последний heartbeat: $hLast`r`n"
|
||||||
$msg += "<i>Проверьте процесс Login_Monitor.ps1 и задачи планировщика RDP-Login-Monitor / Watchdog.</i>"
|
$msg += "<i>Проверьте процесс Login_Monitor.ps1 и задачи планировщика RDP-Login-Monitor / Watchdog.</i>"
|
||||||
|
|
||||||
if (Send-MonitorNotification -Message $msg -EmailSubject 'RDP Login Monitor: нет heartbeat') {
|
if (Send-MonitorNotification -Message $msg -EmailSubject 'RDP Login Monitor: нет heartbeat' `
|
||||||
|
-SacEventType 'agent.health' -SacSeverity 'warning' `
|
||||||
|
-SacTitle 'RDP monitor heartbeat stale' `
|
||||||
|
-SacSummary "Heartbeat не обновлялся на $env:COMPUTERNAME") {
|
||||||
$script:HeartbeatStaleAlertActive = $true
|
$script:HeartbeatStaleAlertActive = $true
|
||||||
Write-Log "Отправлено оповещение: heartbeat не обновлялся дольше $thresholdSec с"
|
Write-Log "Отправлено оповещение: heartbeat не обновлялся дольше $thresholdSec с"
|
||||||
}
|
}
|
||||||
@@ -1286,7 +1360,11 @@ function Send-StopNotification {
|
|||||||
$message += "🕐 Время остановки: $timestamp`r`n"
|
$message += "🕐 Время остановки: $timestamp`r`n"
|
||||||
$message += "📋 Причина: $hReason"
|
$message += "📋 Причина: $hReason"
|
||||||
|
|
||||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: остановка" | Out-Null
|
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: остановка" `
|
||||||
|
-SacEventType 'agent.lifecycle' -SacSeverity 'info' `
|
||||||
|
-SacTitle 'RDP login monitor stopped' `
|
||||||
|
-SacSummary "Мониторинг остановлен: $Reason" `
|
||||||
|
-SacDetails @{ lifecycle = 'stopped'; reason = $Reason } | Out-Null
|
||||||
Write-Log "Уведомление об остановке отправлено: $Reason"
|
Write-Log "Уведомление об остановке отправлено: $Reason"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2181,7 +2259,20 @@ function Send-DailyReport {
|
|||||||
} else {
|
} else {
|
||||||
$message += "`r`n<i>Список пользователей недоступен (quser пуст или недостаточно прав).</i>"
|
$message += "`r`n<i>Список пользователей недоступен (quser пуст или недостаточно прав).</i>"
|
||||||
}
|
}
|
||||||
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: ежедневный отчёт" | Out-Null
|
$rdpReportDetails = @{
|
||||||
|
stats = @{
|
||||||
|
active_sessions_rdp = $count
|
||||||
|
unique_users = @($uniqueUsers)
|
||||||
|
}
|
||||||
|
report_body = (Get-PlainSummaryFromTelegramHtml -Message $message)
|
||||||
|
report_html = "<div class=""agent-report"">$message</div>"
|
||||||
|
report_format = 'telegram_html'
|
||||||
|
}
|
||||||
|
Send-MonitorNotification -Message $message -EmailSubject "RDP Login Monitor: ежедневный отчёт" `
|
||||||
|
-SacEventType 'report.daily.rdp' -SacSeverity 'info' `
|
||||||
|
-SacTitle 'RDP daily report' `
|
||||||
|
-SacSummary "RDP 24ч: сессий $count, логинов $($uniqueUsers.Count)" `
|
||||||
|
-SacDetails $rdpReportDetails | Out-Null
|
||||||
Write-TextFileUtf8Bom -Path $LastReportFile -Text ((Get-Date).ToString("yyyy-MM-dd HH:mm:ss"))
|
Write-TextFileUtf8Bom -Path $LastReportFile -Text ((Get-Date).ToString("yyyy-MM-dd HH:mm:ss"))
|
||||||
Write-Log "Ежедневный отчет отправлен"
|
Write-Log "Ежедневный отчет отправлен"
|
||||||
return $true
|
return $true
|
||||||
@@ -2483,7 +2574,17 @@ function Start-LoginMonitor {
|
|||||||
|
|
||||||
Write-Log "Notify: ID=4625 User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP) (tier1=$($rl.UserIpCount) tier2=$($rl.IpCount))"
|
Write-Log "Notify: ID=4625 User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP) (tier1=$($rl.UserIpCount) tier2=$($rl.IpCount))"
|
||||||
Send-MonitorNotification -Message $formattedMessage `
|
Send-MonitorNotification -Message $formattedMessage `
|
||||||
-EmailSubject "RDP Login Monitor: неудачный вход (4625)" | Out-Null
|
-EmailSubject "RDP Login Monitor: неудачный вход (4625)" `
|
||||||
|
-SacEventType 'rdp.login.failed' -SacSeverity 'warning' `
|
||||||
|
-SacTitle 'RDP login failed' `
|
||||||
|
-SacSummary "4625 $($eventInfo.Username) from $($eventInfo.SourceIP)" `
|
||||||
|
-SacDetails @{
|
||||||
|
user = $eventInfo.Username
|
||||||
|
ip_address = $eventInfo.SourceIP
|
||||||
|
logon_type = $eventInfo.LogonType
|
||||||
|
event_id_windows = 4625
|
||||||
|
workstation_name = $eventInfo.ComputerName
|
||||||
|
} | Out-Null
|
||||||
} else {
|
} else {
|
||||||
Write-Log "Notify suppressed 4625: User=$($eventInfo.Username) IP=$($eventInfo.SourceIP) tier1=$($rl.UserIpCount)/$FailedLogonRateLimitUserIpThreshold tier2=$($rl.IpCount)/$FailedLogonRateLimitIpThreshold"
|
Write-Log "Notify suppressed 4625: User=$($eventInfo.Username) IP=$($eventInfo.SourceIP) tier1=$($rl.UserIpCount)/$FailedLogonRateLimitUserIpThreshold tier2=$($rl.IpCount)/$FailedLogonRateLimitIpThreshold"
|
||||||
}
|
}
|
||||||
@@ -2492,7 +2593,11 @@ function Start-LoginMonitor {
|
|||||||
$tierLabel = if ($burst.Tier -eq 'UserIp') { 'IP+user' } else { 'IP' }
|
$tierLabel = if ($burst.Tier -eq 'UserIp') { 'IP+user' } else { 'IP' }
|
||||||
Write-Log "Notify burst 4625 ($tierLabel): count=$($burst.Count) key=$($burst.BucketKey)"
|
Write-Log "Notify burst 4625 ($tierLabel): count=$($burst.Count) key=$($burst.BucketKey)"
|
||||||
Send-MonitorNotification -Message $burst.Message `
|
Send-MonitorNotification -Message $burst.Message `
|
||||||
-EmailSubject "RDP Login Monitor: брутфорс 4625 ($tierLabel)" | Out-Null
|
-EmailSubject "RDP Login Monitor: брутфорс 4625 ($tierLabel)" `
|
||||||
|
-SacEventType 'rdp.bruteforce.burst' -SacSeverity 'high' `
|
||||||
|
-SacTitle "RDP bruteforce burst ($tierLabel)" `
|
||||||
|
-SacSummary "4625 burst tier=$tierLabel count=$($burst.Count)" `
|
||||||
|
-SacDetails @{ tier = $tierLabel; count = $burst.Count; bucket_key = $burst.BucketKey } | Out-Null
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
$formattedMessage = Format-LoginEvent -EventID $event.Id `
|
$formattedMessage = Format-LoginEvent -EventID $event.Id `
|
||||||
@@ -2506,8 +2611,19 @@ function Start-LoginMonitor {
|
|||||||
-SecurityLogComputerName $event.MachineName
|
-SecurityLogComputerName $event.MachineName
|
||||||
|
|
||||||
Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
|
Write-Log "Notify: ID=$($event.Id) User=$($eventInfo.Username) LT=$($eventInfo.LogonType) IP=$($eventInfo.SourceIP)"
|
||||||
|
$sacType = if ($event.Id -eq 4624) { 'rdp.login.success' } else { 'agent.notification' }
|
||||||
Send-MonitorNotification -Message $formattedMessage `
|
Send-MonitorNotification -Message $formattedMessage `
|
||||||
-EmailSubject "RDP Login Monitor: вход (ID $($event.Id))" | Out-Null
|
-EmailSubject "RDP Login Monitor: вход (ID $($event.Id))" `
|
||||||
|
-SacEventType $sacType -SacSeverity 'info' `
|
||||||
|
-SacTitle "RDP login event $($event.Id)" `
|
||||||
|
-SacSummary "Event $($event.Id) $($eventInfo.Username) from $($eventInfo.SourceIP)" `
|
||||||
|
-SacDetails @{
|
||||||
|
user = $eventInfo.Username
|
||||||
|
ip_address = $eventInfo.SourceIP
|
||||||
|
logon_type = $eventInfo.LogonType
|
||||||
|
event_id_windows = [int]$event.Id
|
||||||
|
workstation_name = $eventInfo.ComputerName
|
||||||
|
} | Out-Null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2534,8 +2650,19 @@ function Start-LoginMonitor {
|
|||||||
-ErrorCode $ei.ErrorCode `
|
-ErrorCode $ei.ErrorCode `
|
||||||
-TimeCreated $ei.TimeCreated
|
-TimeCreated $ei.TimeCreated
|
||||||
Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username)"
|
Write-Log "Notify RDG: ID=$($event.Id) User=$($ei.Username)"
|
||||||
|
$rdgType = if ($event.Id -eq 302) { 'rdg.connection.success' } else { 'rdg.connection.failed' }
|
||||||
|
$rdgSev = if ($event.Id -eq 302) { 'info' } else { 'warning' }
|
||||||
Send-MonitorNotification -Message $msg `
|
Send-MonitorNotification -Message $msg `
|
||||||
-EmailSubject "RDP Login Monitor: RD Gateway ($($event.Id))" | Out-Null
|
-EmailSubject "RDP Login Monitor: RD Gateway ($($event.Id))" `
|
||||||
|
-SacEventType $rdgType -SacSeverity $rdgSev `
|
||||||
|
-SacTitle "RD Gateway event $($event.Id)" `
|
||||||
|
-SacSummary "RDG $($event.Id) $($ei.Username)" `
|
||||||
|
-SacDetails @{
|
||||||
|
user = $ei.Username
|
||||||
|
external_ip = $ei.ExternalIP
|
||||||
|
internal_ip = $ei.InternalIP
|
||||||
|
event_id_windows = [int]$event.Id
|
||||||
|
} | Out-Null
|
||||||
}
|
}
|
||||||
$lastGatewayCheckTime = ($gatewayEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
$lastGatewayCheckTime = ($gatewayEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||||
}
|
}
|
||||||
@@ -2560,7 +2687,15 @@ function Start-LoginMonitor {
|
|||||||
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
-TimeCreated $rcmInfo.TimeCreated -SecurityLogComputerName $event.MachineName
|
||||||
Write-Log "Notify RCM 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP)"
|
Write-Log "Notify RCM 1149: User=$($rcmInfo.Username) IP=$($rcmInfo.ClientIP)"
|
||||||
Send-MonitorNotification -Message $msg `
|
Send-MonitorNotification -Message $msg `
|
||||||
-EmailSubject "RDP Login Monitor: RDP 1149" | Out-Null
|
-EmailSubject "RDP Login Monitor: RDP 1149" `
|
||||||
|
-SacEventType 'rdp.login.success' -SacSeverity 'info' `
|
||||||
|
-SacTitle 'RDP connection (RCM 1149)' `
|
||||||
|
-SacSummary "RCM 1149 $($rcmInfo.Username) $($rcmInfo.ClientIP)" `
|
||||||
|
-SacDetails @{
|
||||||
|
user = $rcmInfo.Username
|
||||||
|
ip_address = $rcmInfo.ClientIP
|
||||||
|
event_id_windows = 1149
|
||||||
|
} | Out-Null
|
||||||
}
|
}
|
||||||
$lastRcmCheckTime = ($rcmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
$lastRcmCheckTime = ($rcmEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||||
}
|
}
|
||||||
@@ -2592,7 +2727,16 @@ function Start-LoginMonitor {
|
|||||||
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
|
-TimeCreated $lo.TimeCreated -IisClientIps $iisIps
|
||||||
Write-Log "Notify 4740: User=$($lo.Username) IIS_IPs=$($iisIps -join ', ')"
|
Write-Log "Notify 4740: User=$($lo.Username) IIS_IPs=$($iisIps -join ', ')"
|
||||||
Send-MonitorNotification -Message $msg `
|
Send-MonitorNotification -Message $msg `
|
||||||
-EmailSubject "RDP Login Monitor: блокировка УЗ $($lo.Username)" | Out-Null
|
-EmailSubject "RDP Login Monitor: блокировка УЗ $($lo.Username)" `
|
||||||
|
-SacEventType 'auth.account.locked' -SacSeverity 'high' `
|
||||||
|
-SacTitle "Account locked $($lo.Username)" `
|
||||||
|
-SacSummary "4740 $($lo.Username)" `
|
||||||
|
-SacDetails @{
|
||||||
|
user = $lo.Username
|
||||||
|
domain = $lo.Domain
|
||||||
|
event_id_windows = 4740
|
||||||
|
iis_client_ips = @($iisIps)
|
||||||
|
} | Out-Null
|
||||||
}
|
}
|
||||||
$lastLockout4740CheckTime = ($lockoutEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
$lastLockout4740CheckTime = ($lockoutEvents | Measure-Object -Property TimeCreated -Maximum | Select-Object -ExpandProperty Maximum).AddSeconds(1)
|
||||||
}
|
}
|
||||||
@@ -2609,6 +2753,9 @@ function Start-LoginMonitor {
|
|||||||
if ($now -ge $nextReportCheck) {
|
if ($now -ge $nextReportCheck) {
|
||||||
$nextReportCheck = Check-AndSendDailyReport
|
$nextReportCheck = Check-AndSendDailyReport
|
||||||
}
|
}
|
||||||
|
if ($script:SacClientLoaded) {
|
||||||
|
Invoke-SacFlushSpool -MaxFiles 5 | Out-Null
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { throw }
|
if ($_.Exception -is [System.Management.Automation.PipelineStoppedException]) { throw }
|
||||||
Write-Log "Ошибка цикла мониторинга: $($_.Exception.Message)"
|
Write-Log "Ошибка цикла мониторинга: $($_.Exception.Message)"
|
||||||
@@ -2619,10 +2766,24 @@ function Start-LoginMonitor {
|
|||||||
|
|
||||||
$script:StopNotificationSent = $false
|
$script:StopNotificationSent = $false
|
||||||
try {
|
try {
|
||||||
|
$sacMode = 'off'
|
||||||
|
if ($script:SacClientLoaded) { $sacMode = Get-SacNormalizedMode }
|
||||||
|
if ($sacMode -ne 'off') {
|
||||||
|
if (-not (Test-SacConfigured)) {
|
||||||
|
Write-Log "ОШИБКА: UseSAC=$sacMode, но SacUrl/SacApiKey не заданы в login_monitor.settings.ps1"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
if (-not (Test-SacHealth)) {
|
||||||
|
Write-Log "ОШИБКА: SAC /health недоступен (SacUrl=$SacUrl)"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
Write-Log "SAC: режим $sacMode, health OK, ingest=$(Get-SacIngestUrl)"
|
||||||
|
}
|
||||||
|
|
||||||
$notifyChannels = @(Get-NotifyOrderChannels)
|
$notifyChannels = @(Get-NotifyOrderChannels)
|
||||||
if ($notifyChannels.Count -eq 0) {
|
if ($notifyChannels.Count -eq 0 -and $sacMode -eq 'off') {
|
||||||
Write-Log "ВНИМАНИЕ: не настроен ни один канал оповещений (Telegram и/или SMTP в конфигурации скрипта)."
|
Write-Log "ВНИМАНИЕ: не настроен ни один канал оповещений (Telegram и/или SMTP в конфигурации скрипта)."
|
||||||
} else {
|
} elseif ($notifyChannels.Count -gt 0) {
|
||||||
foreach ($notifyCh in $notifyChannels) {
|
foreach ($notifyCh in $notifyChannels) {
|
||||||
switch ($notifyCh) {
|
switch ($notifyCh) {
|
||||||
'telegram' { Test-TelegramConnection | Out-Null }
|
'telegram' { Test-TelegramConnection | Out-Null }
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ PowerShell-набор для мониторинга входов в Windows с
|
|||||||
- Документация по развёртыванию: **[Docs/README.md](Docs/README.md)** (RDP-монитор, Exchange, NETLOGON).
|
- Документация по развёртыванию: **[Docs/README.md](Docs/README.md)** (RDP-монитор, Exchange, NETLOGON).
|
||||||
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id и пароля SMTP (`$MailSmtpPasswordProtectedB64` в файле настроек).
|
- **`Encrypt-DpapiForRdpMonitor.ps1`** — опционально для подготовки DPAPI-строк токена/chat id и пароля SMTP (`$MailSmtpPasswordProtectedB64` в файле настроек).
|
||||||
- **Локальные настройки RDP-монитора:** **`login_monitor.settings.ps1`** в каталоге установки (образец **`login_monitor.settings.example.ps1`**). При автообновлении **`Login_Monitor.ps1`** с шары файл настроек **не перезаписывается** (как **`exchange_monitor.settings.ps1`** для Exchange).
|
- **Локальные настройки RDP-монитора:** **`login_monitor.settings.ps1`** в каталоге установки (образец **`login_monitor.settings.example.ps1`**). При автообновлении **`Login_Monitor.ps1`** с шары файл настроек **не перезаписывается** (как **`exchange_monitor.settings.ps1`** для Exchange).
|
||||||
|
- **Security Alert Center (SAC):** модуль **`Sac-Client.ps1`** (копируется вместе с `Login_Monitor.ps1`). Режимы **`$UseSAC`**: `off` | `exclusive` | `dual` | `fallback` — контракт в репозитории **security-alert-center** (`docs/agent-integration.md`). Версия релиза агента: **`1.2.0-SAC`** (`$ScriptVersion` и `version.txt`).
|
||||||
|
|
||||||
## Что изменилось (важное)
|
## Что изменилось (важное)
|
||||||
|
|
||||||
@@ -30,12 +31,14 @@ PowerShell-набор для мониторинга входов в Windows с
|
|||||||
- `C:\ProgramData\RDP-login-monitor\`
|
- `C:\ProgramData\RDP-login-monitor\`
|
||||||
2. Скопируйте в неё как минимум:
|
2. Скопируйте в неё как минимум:
|
||||||
- `Login_Monitor.ps1`
|
- `Login_Monitor.ps1`
|
||||||
|
- `Sac-Client.ps1`
|
||||||
- `login_monitor.settings.example.ps1` → переименуйте в **`login_monitor.settings.ps1`** и задайте параметры (см. п. 3)
|
- `login_monitor.settings.example.ps1` → переименуйте в **`login_monitor.settings.ps1`** и задайте параметры (см. п. 3)
|
||||||
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1`, `version.txt` и `login_monitor.settings.example.ps1`
|
- (для доменного развёртывания отдельно на шаре) `Deploy-LoginMonitor.ps1`, `version.txt` и `login_monitor.settings.example.ps1`
|
||||||
3. Настройте **`C:\ProgramData\RDP-login-monitor\login_monitor.settings.ps1`** (не редактируйте секреты в `Login_Monitor.ps1` — они перезапишутся при деплое):
|
3. Настройте **`C:\ProgramData\RDP-login-monitor\login_monitor.settings.ps1`** (не редактируйте секреты в `Login_Monitor.ps1` — они перезапишутся при деплое):
|
||||||
- **Telegram:** `$TelegramBotToken` / `$TelegramChatID` или `...ProtectedB64`
|
- **Telegram:** `$TelegramBotToken` / `$TelegramChatID` или `...ProtectedB64`
|
||||||
- **Email (SMTP):** `$MailSmtpHost`, `$MailFrom`, `$MailTo`, `$MailSmtpPort` (по умолчанию 587), при необходимости `$MailSmtpUser` / `$MailSmtpPassword` (или `$MailSmtpPasswordProtectedB64` через DPAPI), `$MailSmtpStartTls` / `$MailSmtpSsl`
|
- **Email (SMTP):** `$MailSmtpHost`, `$MailFrom`, `$MailTo`, `$MailSmtpPort` (по умолчанию 587), при необходимости `$MailSmtpUser` / `$MailSmtpPassword` (или `$MailSmtpPasswordProtectedB64` через DPAPI), `$MailSmtpStartTls` / `$MailSmtpSsl`
|
||||||
- **Порядок:** `$NotifyOrder` — пусто = авто (Telegram → Email, только настроенные); иначе `telegram,email` или `email` и т.п. (допускаются `tg`, `mail`)
|
- **Порядок:** `$NotifyOrder` — пусто = авто (Telegram → Email, только настроенные); иначе `telegram,email` или `email` и т.п. (допускаются `tg`, `mail`)
|
||||||
|
- **SAC (опционально):** `$UseSAC`, `$SacUrl`, `$SacApiKey` — см. блок в **`login_monitor.settings.example.ps1`**. Проверка: `Login_Monitor.ps1 -CheckSac`
|
||||||
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
|
4. Запускайте с правами администратора (чтение `Security` журнала и регистрация задач).
|
||||||
5. Логи и служебные файлы будут в:
|
5. Логи и служебные файлы будут в:
|
||||||
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
- `C:\ProgramData\RDP-login-monitor\Logs\`
|
||||||
|
|||||||
+387
@@ -0,0 +1,387 @@
|
|||||||
|
<#
|
||||||
|
.SYNOPSIS
|
||||||
|
Клиент Security Alert Center для RDP-login-monitor.
|
||||||
|
.DESCRIPTION
|
||||||
|
Dot-source после login_monitor.settings.ps1 и функции Write-Log.
|
||||||
|
Ожидает: $UseSAC, $SacUrl, $SacApiKey, $ScriptVersion, $script:InstallRoot.
|
||||||
|
#>
|
||||||
|
|
||||||
|
function Write-SacLog {
|
||||||
|
param([string]$Message)
|
||||||
|
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
||||||
|
Write-Log $Message
|
||||||
|
} else {
|
||||||
|
Write-Host $Message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacNormalizedMode {
|
||||||
|
$m = if ($null -ne $UseSAC) { [string]$UseSAC } else { 'off' }
|
||||||
|
return $m.Trim().ToLowerInvariant()
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SacConfigured {
|
||||||
|
return (-not [string]::IsNullOrWhiteSpace($SacUrl)) -and (-not [string]::IsNullOrWhiteSpace($SacApiKey))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacBaseUrl {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($SacUrl)) { return $null }
|
||||||
|
$url = $SacUrl.Trim().TrimEnd('/')
|
||||||
|
if ($url -match '/api/v1/events$') {
|
||||||
|
$url = $url -replace '/api/v1/events$', ''
|
||||||
|
}
|
||||||
|
return $url.TrimEnd('/')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacIngestUrl {
|
||||||
|
$base = Get-SacBaseUrl
|
||||||
|
if ([string]::IsNullOrWhiteSpace($base)) { return $null }
|
||||||
|
return "$base/api/v1/events"
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacSpoolDirResolved {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SacSpoolDir)) {
|
||||||
|
return $SacSpoolDir.Trim()
|
||||||
|
}
|
||||||
|
return (Join-Path $script:InstallRoot 'sac-spool')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacAgentIdFileResolved {
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($SacAgentIdFile)) {
|
||||||
|
return $SacAgentIdFile.Trim()
|
||||||
|
}
|
||||||
|
return (Join-Path $script:InstallRoot 'agent_instance_id')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacFailCountFileResolved {
|
||||||
|
return (Join-Path $script:InstallRoot 'sac-fail.count')
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacAgentInstanceId {
|
||||||
|
$idFile = Get-SacAgentIdFileResolved
|
||||||
|
$dir = Split-Path -Parent $idFile
|
||||||
|
if (-not (Test-Path -LiteralPath $dir)) {
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
}
|
||||||
|
if (Test-Path -LiteralPath $idFile) {
|
||||||
|
$existing = (Get-Content -LiteralPath $idFile -TotalCount 1 -ErrorAction SilentlyContinue)
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($existing)) {
|
||||||
|
return $existing.Trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$newId = [guid]::NewGuid().ToString()
|
||||||
|
try {
|
||||||
|
Set-Content -LiteralPath $idFile -Value $newId -Encoding UTF8 -NoNewline
|
||||||
|
} catch { }
|
||||||
|
return $newId
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacCategoryForType {
|
||||||
|
param([string]$EventType)
|
||||||
|
if ($EventType -match '^(ssh\.|auth\.|rdp\.)') { return 'auth' }
|
||||||
|
if ($EventType -match '^privilege\.') { return 'privilege' }
|
||||||
|
if ($EventType -match '^session\.') { return 'session' }
|
||||||
|
if ($EventType -match '^report\.') { return 'report' }
|
||||||
|
if ($EventType -match '^rdg\.') { return 'network' }
|
||||||
|
return 'agent'
|
||||||
|
}
|
||||||
|
|
||||||
|
function New-SacEventPayload {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$EventType,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Severity,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Title,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Summary,
|
||||||
|
[hashtable]$Details = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
$payload = [ordered]@{
|
||||||
|
schema_version = '1.0'
|
||||||
|
event_id = [guid]::NewGuid().ToString()
|
||||||
|
occurred_at = (Get-Date).ToString('o')
|
||||||
|
source = [ordered]@{
|
||||||
|
product = 'rdp-login-monitor'
|
||||||
|
product_version = if ($ScriptVersion) { [string]$ScriptVersion } else { 'unknown' }
|
||||||
|
agent_instance_id = Get-SacAgentInstanceId
|
||||||
|
}
|
||||||
|
host = [ordered]@{
|
||||||
|
hostname = $env:COMPUTERNAME
|
||||||
|
os_family = 'windows'
|
||||||
|
}
|
||||||
|
category = (Get-SacCategoryForType -EventType $EventType)
|
||||||
|
type = $EventType
|
||||||
|
severity = $Severity
|
||||||
|
title = $Title
|
||||||
|
summary = $Summary
|
||||||
|
}
|
||||||
|
if ($null -ne $Details -and $Details.Count -gt 0) {
|
||||||
|
$payload.details = $Details
|
||||||
|
}
|
||||||
|
return $payload
|
||||||
|
}
|
||||||
|
|
||||||
|
function Get-SacFailCount {
|
||||||
|
$f = Get-SacFailCountFileResolved
|
||||||
|
if (-not (Test-Path -LiteralPath $f)) { return 0 }
|
||||||
|
$raw = (Get-Content -LiteralPath $f -TotalCount 1 -ErrorAction SilentlyContinue) -replace '\D', ''
|
||||||
|
if ([string]::IsNullOrWhiteSpace($raw)) { return 0 }
|
||||||
|
return [int]$raw
|
||||||
|
}
|
||||||
|
|
||||||
|
function Set-SacFailCount {
|
||||||
|
param([int]$Count)
|
||||||
|
$f = Get-SacFailCountFileResolved
|
||||||
|
$dir = Split-Path -Parent $f
|
||||||
|
if (-not (Test-Path -LiteralPath $dir)) {
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
}
|
||||||
|
Set-Content -LiteralPath $f -Value ([string]$Count) -Encoding UTF8 -NoNewline
|
||||||
|
}
|
||||||
|
|
||||||
|
function Reset-SacFailCount {
|
||||||
|
Set-SacFailCount -Count 0
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SacShouldAttemptSend {
|
||||||
|
$mode = Get-SacNormalizedMode
|
||||||
|
if ($mode -ne 'fallback') { return $true }
|
||||||
|
|
||||||
|
$max = if ($SacFallbackFailures) { [int]$SacFallbackFailures } else { 5 }
|
||||||
|
$n = Get-SacFailCount
|
||||||
|
if ($n -lt $max) { return $true }
|
||||||
|
|
||||||
|
if (Test-SacHealth) {
|
||||||
|
Reset-SacFailCount
|
||||||
|
Write-SacLog 'SAC: /health OK, resuming POST (fallback)'
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
Write-SacLog "WARN: SAC fallback: skip POST ($n>=$max failures), local channels only"
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SacTlsPrep {
|
||||||
|
if (-not $SacTlsSkipVerify) { return }
|
||||||
|
if (-not $script:SacTlsCallbackRegistered) {
|
||||||
|
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
|
||||||
|
$script:SacTlsCallbackRegistered = $true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SacHealth {
|
||||||
|
if (-not (Test-SacConfigured)) { return $false }
|
||||||
|
$base = Get-SacBaseUrl
|
||||||
|
if ([string]::IsNullOrWhiteSpace($base)) { return $false }
|
||||||
|
|
||||||
|
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||||
|
try {
|
||||||
|
Invoke-SacTlsPrep
|
||||||
|
$resp = Invoke-WebRequest -Uri "$base/health" -Method Get -UseBasicParsing -TimeoutSec $timeout
|
||||||
|
return ($resp.StatusCode -eq 200)
|
||||||
|
} catch {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-SacSpoolFile {
|
||||||
|
param(
|
||||||
|
[string]$EventId,
|
||||||
|
[string]$JsonBody
|
||||||
|
)
|
||||||
|
$dir = Get-SacSpoolDirResolved
|
||||||
|
if (-not (Test-Path -LiteralPath $dir)) {
|
||||||
|
New-Item -ItemType Directory -Path $dir -Force | Out-Null
|
||||||
|
}
|
||||||
|
$path = Join-Path $dir "$EventId.json"
|
||||||
|
[System.IO.File]::WriteAllText($path, $JsonBody, (New-Object System.Text.UTF8Encoding $false))
|
||||||
|
}
|
||||||
|
|
||||||
|
function Remove-SacSpoolFile {
|
||||||
|
param([string]$EventId)
|
||||||
|
$path = Join-Path (Get-SacSpoolDirResolved) "$EventId.json"
|
||||||
|
if (Test-Path -LiteralPath $path) {
|
||||||
|
Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SacPostPayload {
|
||||||
|
param([string]$JsonBody)
|
||||||
|
|
||||||
|
if (-not (Test-SacConfigured)) { return $false }
|
||||||
|
if (-not (Test-SacShouldAttemptSend)) { return $false }
|
||||||
|
|
||||||
|
$ingest = Get-SacIngestUrl
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ingest)) { return $false }
|
||||||
|
|
||||||
|
$obj = $JsonBody | ConvertFrom-Json
|
||||||
|
$eventId = [string]$obj.event_id
|
||||||
|
$eventType = [string]$obj.type
|
||||||
|
$timeout = if ($SacTimeoutSec) { [int]$SacTimeoutSec } else { 12 }
|
||||||
|
|
||||||
|
try {
|
||||||
|
Invoke-SacTlsPrep
|
||||||
|
$headers = @{
|
||||||
|
Authorization = "Bearer $SacApiKey"
|
||||||
|
'Content-Type' = 'application/json'
|
||||||
|
'Idempotency-Key' = $eventId
|
||||||
|
}
|
||||||
|
$resp = Invoke-WebRequest -Uri $ingest -Method Post -Headers $headers -Body $JsonBody -UseBasicParsing -TimeoutSec $timeout
|
||||||
|
if ($resp.StatusCode -eq 202) {
|
||||||
|
Remove-SacSpoolFile -EventId $eventId
|
||||||
|
Reset-SacFailCount
|
||||||
|
Write-SacLog "SAC: accepted event_id=$eventId type=$eventType"
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
$body = if ($resp.Content) { $resp.Content } else { '' }
|
||||||
|
Write-SacLog "WARN: SAC POST HTTP $($resp.StatusCode): $($body.Substring(0, [Math]::Min(200, $body.Length)))"
|
||||||
|
} catch {
|
||||||
|
$code = 0
|
||||||
|
if ($_.Exception.Response) {
|
||||||
|
$code = [int]$_.Exception.Response.StatusCode
|
||||||
|
}
|
||||||
|
$err = $_.Exception.Message
|
||||||
|
Write-SacLog "WARN: SAC POST HTTP ${code}: $err"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-SacSpoolFile -EventId $eventId -JsonBody $JsonBody
|
||||||
|
$max = if ($SacFallbackFailures) { [int]$SacFallbackFailures } else { 5 }
|
||||||
|
$n = Get-SacFailCount + 1
|
||||||
|
Set-SacFailCount -Count $n
|
||||||
|
if ($n -ge $max) {
|
||||||
|
Write-SacLog "WARN: SAC fallback: SAC_FALLBACK_FAILURES threshold ($max)"
|
||||||
|
}
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-SacEvent {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$EventType,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Severity,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Title,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Summary,
|
||||||
|
[hashtable]$Details = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (-not (Test-SacConfigured)) {
|
||||||
|
Write-SacLog 'WARN: SAC not configured (SacUrl / SacApiKey)'
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
$payload = New-SacEventPayload -EventType $EventType -Severity $Severity -Title $Title -Summary $Summary -Details $Details
|
||||||
|
$json = $payload | ConvertTo-Json -Depth 8 -Compress
|
||||||
|
return (Invoke-SacPostPayload -JsonBody $json)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-SacLocalChannels {
|
||||||
|
param(
|
||||||
|
[string]$TelegramMessage,
|
||||||
|
[string]$EmailSubject
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($TelegramMessage)) { return $false }
|
||||||
|
|
||||||
|
$channels = @(Get-NotifyOrderChannels)
|
||||||
|
if ($channels.Count -eq 0) { return $false }
|
||||||
|
|
||||||
|
$anyOk = $false
|
||||||
|
foreach ($ch in $channels) {
|
||||||
|
$ok = switch ($ch) {
|
||||||
|
'telegram' { Send-TelegramMessage -Message $TelegramMessage }
|
||||||
|
'email' { Send-EmailNotification -Message $TelegramMessage -Subject $EmailSubject }
|
||||||
|
default { $false }
|
||||||
|
}
|
||||||
|
if ($ok) { $anyOk = $true }
|
||||||
|
}
|
||||||
|
return $anyOk
|
||||||
|
}
|
||||||
|
|
||||||
|
function Send-NotifyOrSac {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)][string]$EventType,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Severity,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Title,
|
||||||
|
[Parameter(Mandatory = $true)][string]$Summary,
|
||||||
|
[string]$TelegramMessage = '',
|
||||||
|
[string]$EmailSubject = 'RDP Login Monitor',
|
||||||
|
[hashtable]$Details = $null
|
||||||
|
)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($TelegramMessage)) {
|
||||||
|
$TelegramMessage = $Summary
|
||||||
|
}
|
||||||
|
|
||||||
|
$mode = Get-SacNormalizedMode
|
||||||
|
switch ($mode) {
|
||||||
|
'off' {
|
||||||
|
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
||||||
|
}
|
||||||
|
'exclusive' {
|
||||||
|
return (Send-SacEvent -EventType $EventType -Severity $Severity -Title $Title -Summary $Summary -Details $Details)
|
||||||
|
}
|
||||||
|
'dual' {
|
||||||
|
Send-SacEvent -EventType $EventType -Severity $Severity -Title $Title -Summary $Summary -Details $Details | Out-Null
|
||||||
|
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
||||||
|
}
|
||||||
|
'fallback' {
|
||||||
|
if (Send-SacEvent -EventType $EventType -Severity $Severity -Title $Title -Summary $Summary -Details $Details) {
|
||||||
|
return $true
|
||||||
|
}
|
||||||
|
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-SacLog "WARN: unknown UseSAC=$mode, local channels only"
|
||||||
|
return (Send-SacLocalChannels -TelegramMessage $TelegramMessage -EmailSubject $EmailSubject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-SacFlushSpool {
|
||||||
|
param([int]$MaxFiles = 20)
|
||||||
|
|
||||||
|
$mode = Get-SacNormalizedMode
|
||||||
|
if ($mode -eq 'off') { return }
|
||||||
|
if (-not (Test-SacConfigured)) { return }
|
||||||
|
|
||||||
|
$dir = Get-SacSpoolDirResolved
|
||||||
|
if (-not (Test-Path -LiteralPath $dir)) { return }
|
||||||
|
|
||||||
|
$files = @(Get-ChildItem -LiteralPath $dir -Filter '*.json' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime)
|
||||||
|
$count = 0
|
||||||
|
foreach ($f in $files) {
|
||||||
|
$count++
|
||||||
|
if ($count -gt $MaxFiles) { break }
|
||||||
|
try {
|
||||||
|
$json = [System.IO.File]::ReadAllText($f.FullName)
|
||||||
|
Invoke-SacPostPayload -JsonBody $json | Out-Null
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-SacConnection {
|
||||||
|
Write-Host 'SAC check (rdp-login-monitor)'
|
||||||
|
Write-Host "UseSAC=$(Get-SacNormalizedMode)"
|
||||||
|
switch (Get-SacNormalizedMode) {
|
||||||
|
'exclusive' { Write-Host 'Mode exclusive: SAC only' }
|
||||||
|
'dual' { Write-Host 'Mode dual: SAC + local channels' }
|
||||||
|
'fallback' { Write-Host 'Mode fallback: SAC, then local on failure' }
|
||||||
|
}
|
||||||
|
Write-Host "SacUrl=$SacUrl"
|
||||||
|
if (Test-SacConfigured) {
|
||||||
|
Write-Host "SAC ingest URL=$(Get-SacIngestUrl)"
|
||||||
|
}
|
||||||
|
if (-not (Test-SacConfigured)) {
|
||||||
|
Write-Error 'SAC: SacUrl or SacApiKey missing'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (Test-SacHealth) {
|
||||||
|
Write-Host 'SAC health: OK'
|
||||||
|
} else {
|
||||||
|
Write-Error 'SAC health: FAIL'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if (Send-SacEvent -EventType 'agent.test' -Severity 'info' -Title 'SAC test' -Summary 'rdp-login-monitor CheckSac') {
|
||||||
|
Write-Host 'SAC ingest agent.test: OK (expected HTTP 202)'
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
Write-Error 'SAC ingest agent.test: FAIL'
|
||||||
|
return 1
|
||||||
|
}
|
||||||
@@ -9,8 +9,8 @@
|
|||||||
#>
|
#>
|
||||||
|
|
||||||
# --- Telegram (или DPAPI Base64 через Encrypt-DpapiForRdpMonitor.ps1) ---
|
# --- Telegram (или DPAPI Base64 через Encrypt-DpapiForRdpMonitor.ps1) ---
|
||||||
$TelegramBotToken = '8239219522:AAEyOZX3cwNfgGOMDkf-mgjTIuoaOh5gF7I'
|
$TelegramBotToken = ''
|
||||||
$TelegramChatID = '2843230'
|
$TelegramChatID = ''
|
||||||
# $TelegramBotTokenProtectedB64 = ''
|
# $TelegramBotTokenProtectedB64 = ''
|
||||||
# $TelegramChatIDProtectedB64 = ''
|
# $TelegramChatIDProtectedB64 = ''
|
||||||
|
|
||||||
@@ -26,6 +26,17 @@ $NotifyOrder = 'tg'
|
|||||||
# $MailSmtpSsl = $false
|
# $MailSmtpSsl = $false
|
||||||
# $MailSmtpPasswordProtectedB64 = ''
|
# $MailSmtpPasswordProtectedB64 = ''
|
||||||
|
|
||||||
|
# --- Security Alert Center (SAC) ---
|
||||||
|
# off | exclusive | dual | fallback — см. security-alert-center/docs/agent-integration.md
|
||||||
|
# $UseSAC = 'exclusive'
|
||||||
|
# $SacUrl = 'https://sac.kalinamall.ru'
|
||||||
|
# $SacApiKey = 'sac_xxxxxxxx'
|
||||||
|
# $SacSpoolDir = 'C:\ProgramData\RDP-login-monitor\sac-spool'
|
||||||
|
# $SacTimeoutSec = 12
|
||||||
|
# $SacTlsSkipVerify = $false
|
||||||
|
# $SacFallbackFailures = 5
|
||||||
|
# Проверка: powershell -File Login_Monitor.ps1 -CheckSac
|
||||||
|
|
||||||
# --- Узкое исключение шумовых сетевых логонов (LogonType=3, Advapi) ---
|
# --- Узкое исключение шумовых сетевых логонов (LogonType=3, Advapi) ---
|
||||||
$IgnoreAdvapiNetworkLogonSourceIps = @(
|
$IgnoreAdvapiNetworkLogonSourceIps = @(
|
||||||
'192.168.160.57'
|
'192.168.160.57'
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ param(
|
|||||||
$ErrorActionPreference = 'Stop'
|
$ErrorActionPreference = 'Stop'
|
||||||
$DistFiles = @(
|
$DistFiles = @(
|
||||||
'Login_Monitor.ps1',
|
'Login_Monitor.ps1',
|
||||||
|
'Sac-Client.ps1',
|
||||||
'version.txt',
|
'version.txt',
|
||||||
'Deploy-LoginMonitor.ps1',
|
'Deploy-LoginMonitor.ps1',
|
||||||
'Exchange-MailSecurity.ps1',
|
'Exchange-MailSecurity.ps1',
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
1.6.8
|
1.2.0-SAC
|
||||||
|
|||||||
Reference in New Issue
Block a user