Загрузка данных


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
}