Загрузка данных
function Get-DCLogonEvents {
<#
.SYNOPSIS
Ищет события 4624 с разбивкой на пакеты (батчи) и сверхподробным логированием.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string[]]$AccountSams,
[Parameter(Mandatory)][string]$DomainName,
[int]$Days = 30,
[int]$MaxEventsPerDC = 100000,
[int]$TimeoutSeconds = 300,
[int]$BatchSize = 10
)
$results = New-Object System.Collections.Generic.List[object]
if (-not $AccountSams -or $AccountSams.Count -eq 0) {
Write-ScanLog " Нет учётных записей — этап пропущен." -Level Warning
return $results
}
# --- 1. Поиск DC ---
Write-ScanLog " Поиск DC для домена '$DomainName'..."
try {
$dcs = @(Get-ADDomainController -Server $DomainName -Filter * -ErrorAction Stop |
Select-Object -ExpandProperty Name)
} catch {
Write-ScanLog " ОШИБКА при получении списка DC: $_" -Level Error
return $results
}
Write-ScanLog " Найдено DC ($($dcs.Count)): $($dcs -join ', ')" -Level Success
# --- 2. Генерация XPath-пакетов (Батчинг) ---
$startTimeUtc = (Get-Date).AddDays(-$Days).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
$logonCond = (@('3','4','5','8','9','10') | ForEach-Object { "Data[@Name='LogonType']='$_'" }) -join ' or '
$batches = @()
$batchIndex = 0
for ($i = 0; $i -lt $AccountSams.Count; $i += $BatchSize) {
$batchIndex++
$endIdx = [Math]::Min($i + $BatchSize - 1, $AccountSams.Count - 1)
$batchUsers = $AccountSams[$i..$endIdx]
$escapedUsers = $batchUsers | ForEach-Object { [System.Security.SecurityElement]::Escape($_) }
$userCond = ($escapedUsers | ForEach-Object { "Data[@Name='TargetUserName']='$_'" }) -join ' or '
$xpath = "*[System[(EventID=4624) and TimeCreated[@SystemTime>='$startTimeUtc']] " +
"and EventData[($userCond) and ($logonCond)]]"
$batches += [PSCustomObject]@{
Index = $batchIndex
XPath = $xpath
Users = $batchUsers
}
}
Write-ScanLog " Сгенерировано $($batches.Count) пакетов (по $BatchSize УЗ). Состав пакетов:"
foreach ($b in $batches) {
Write-ScanLog " Пакет $($b.Index)/$($batches.Count): $($b.Users -join ', ')"
}
Write-ScanLog " Запуск параллельного опроса $($dcs.Count) DC (таймаут ${TimeoutSeconds}с на операцию)..."
# --- 3. Настройка сессии и запуск фоновых задач ---
$sessionOpt = New-PSSessionOption `
-OpenTimeout ($TimeoutSeconds * 1000) `
-OperationTimeout ($TimeoutSeconds * 1000) `
-CancelTimeout ($TimeoutSeconds * 1000) `
-IdleTimeout ($TimeoutSeconds * 2000)
$jobs = @()
try {
$jobs = @(Invoke-Command -ComputerName $dcs -SessionOption $sessionOpt `
-ArgumentList $batches, $MaxEventsPerDC `
-ScriptBlock {
param($batches, $max)
$total = $batches.Count
foreach ($b in $batches) {
$batchTimer = [System.Diagnostics.Stopwatch]::StartNew()
Write-Output "[PROGRESS] Пакет $($b.Index)/$total - СТАРТ (УЗ: $($b.Users -join ', '))"
try {
$events = @(Get-WinEvent -LogName 'Security' -FilterXPath $b.XPath -MaxEvents $max -ErrorAction Stop)
$batchTimer.Stop()
Write-Output "[PROGRESS] Пакет $($b.Index)/$total - ГОТОВО (Найдено: $($events.Count), Время: $($batchTimer.Elapsed.TotalSeconds.ToString('0.0'))с)"
# Проверяем, не достигли ли лимита MaxEvents
if ($events.Count -eq $max) {
Write-Output "[WARNING] Пакет $($b.Index)/$total - ДОСТИГНУТ ЛИМИТ MaxEvents ($max)! Возможно, данные обрезаны."
}
} catch {
$batchTimer.Stop()
if ($_.FullyQualifiedErrorId -match 'NoMatchingEventsFound') {
Write-Output "[PROGRESS] Пакет $($b.Index)/$total - ГОТОВО (Найдено: 0, Время: $($batchTimer.Elapsed.TotalSeconds.ToString('0.0'))с)"
$events = @()
} else {
Write-Output "[WARNING] Пакет $($b.Index)/$total - ОШИБКА: $($_.Exception.Message)"
$events = @()
}
}
# Отдаем только нужные поля
foreach ($e in $events) {
[PSCustomObject]@{
TargetUser = $e.Properties[5].Value
LogonType = $e.Properties[8].Value
SourceHost = $e.Properties[18].Value
}
}
}
Write-Output "[PROGRESS] Все пакеты обработаны."
} -AsJob)
} catch {
Write-ScanLog " ОШИБКА запуска заданий: $_" -Level Error
return $results
}
# --- 4. Ожидание с чтением логов из фоновых задач (Real-time polling) ---
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$lastSeen = @{}
foreach ($job in $jobs) { $lastSeen[$job.Id] = 0 }
$globalTimeout = $TimeoutSeconds + 120
while ($true) {
$running = @($jobs | Where-Object { $_.State -eq 'Running' })
if ($running.Count -eq 0) { break }
if ($sw.Elapsed.TotalSeconds -ge $globalTimeout) {
Write-ScanLog " ГЛОБАЛЬНЫЙ ТАЙМАУТ ($globalTimeout с) — прерываем оставшиеся задания." -Level Warning
$running | Stop-Job -ErrorAction SilentlyContinue
break
}
foreach ($job in $running) {
$dcName = $job.Location
$output = Receive-Job -Job $job -Keep -ErrorAction SilentlyContinue
for ($i = $lastSeen[$job.Id]; $i -lt $output.Count; $i++) {
$item = $output[$i]
if ($item -is [string]) {
if ($item -match '^\[PROGRESS\] (.*)') {
Write-ScanLog " [$dcName] $($Matches[1])" -Level Info
} elseif ($item -match '^\[WARNING\] (.*)') {
Write-ScanLog " [$dcName] $($Matches[1])" -Level Warning
}
}
}
$lastSeen[$job.Id] = $output.Count
}
Start-Sleep -Seconds 1 # Уменьшили задержку для более быстрого отображения
}
$sw.Stop()
# --- 5. Сбор финальных результатов ---
$rawResults = @()
Write-ScanLog " --- Итоги по контроллерам домена ---"
foreach ($job in $jobs) {
$dcName = $job.Location
$finalOut = @(Receive-Job -Job $job -ErrorAction SilentlyContinue -ErrorVariable dcErr)
$objects = @($finalOut | Where-Object { $_ -isnot [string] })
$rawResults += $objects
foreach ($e in $dcErr) {
Write-ScanLog " [$dcName] Критическая ошибка: $($e.Exception.Message)" -Level Error
}
if ($job.State -eq 'Failed') {
Write-ScanLog " [$dcName] Завершился с ошибкой." -Level Error
} elseif ($job.State -eq 'Stopped') {
Write-ScanLog " [$dcName] Принудительно остановлен (таймаут)." -Level Warning
} else {
Write-ScanLog " [$dcName] Успешно завершен. Собрано событий: $($objects.Count)" -Level Success
}
Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
}
Write-ScanLog " Всего получено событий со всех DC: $($rawResults.Count) за $($sw.Elapsed.TotalSeconds.ToString('0.0'))с"
# --- 6. Фильтрация и формирование итогового списка ---
$validSams = [System.Collections.Generic.HashSet[string]]::new([string[]]$AccountSams, [System.StringComparer]::OrdinalIgnoreCase)
foreach ($ev in $rawResults) {
if (-not $ev.TargetUser -or -not $validSams.Contains($ev.TargetUser)) { continue }
$src = $ev.SourceHost
if (-not $src -or $src -eq '-') { continue }
$src = $src -replace '^\\\\', ''
$results.Add([PSCustomObject]@{
AccountSam = $ev.TargetUser
Server = $src
WhereUsed = "Журнал входа DC (4624, logon type $($ev.LogonType))"
})
}
return $results
}