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


function Get-DCLogonEvents {
    [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 '

    $xPaths = @()
    for ($i = 0; $i -lt $AccountSams.Count; $i += $BatchSize) {
        $endIdx = [Math]::Min($i + $BatchSize - 1, $AccountSams.Count - 1)
        $batch = $AccountSams[$i..$endIdx]
        
        $escapedUsers = $batch | 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)]]"
                 
        $xPaths += [PSCustomObject]@{
            XPath    = $xpath
            Accounts = $batch
        }
    }

    Write-ScanLog "  Сгенерировано $($xPaths.Count) XPath-пакетов (по $BatchSize УЗ в каждом)."
    Write-ScanLog "  Отправка фонового задания на $($dcs.Count) DC..." -Level Header

    # --- 3. Настройка сессии и запуск фоновых задач ---
    $sessionOpt = New-PSSessionOption `
        -OpenTimeout      ($TimeoutSeconds * 1000) `
        -OperationTimeout ($TimeoutSeconds * 1000) `
        -CancelTimeout    ($TimeoutSeconds * 1000) `
        -IdleTimeout      ($TimeoutSeconds * 2000)

    try {
        # ВАЖНО: (,$xPaths) предотвращает "разворачивание" массива при передаче в -ArgumentList
        $parentJob = Invoke-Command -ComputerName $dcs -SessionOption $sessionOpt `
            -ArgumentList (,$xPaths), $MaxEventsPerDC `
            -ScriptBlock {
                param($xpaths, $max)
                $batchNum = 0
                $total = $xpaths.Count
                $dcName = $env:COMPUTERNAME
                
                foreach ($item in $xpaths) {
                    $batchNum++
                    $accList = $item.Accounts -join ', '
                    if ($accList.Length -gt 60) { $accList = $accList.Substring(0, 57) + "..." }
                    
                    Write-Output "[PROGRESS] [$dcName] Запрос пакета $batchNum/$total (УЗ: $accList)"
                    
                    try {
                        $events = @(Get-WinEvent -LogName 'Security' -FilterXPath $item.XPath -MaxEvents $max -ErrorAction Stop)
                        Write-Output "[PROGRESS] [$dcName] Пакет $batchNum/$total - найдено событий: $($events.Count)"
                    } catch {
                        if ($_.FullyQualifiedErrorId -match 'NoMatchingEventsFound') {
                            $events = @()
                            Write-Output "[PROGRESS] [$dcName] Пакет $batchNum/$total - событий нет"
                        } else {
                            $events = @()
                            Write-Output "[WARNING] [$dcName] Пакет $batchNum/$total - ОШИБКА: $($_.Exception.Message)"
                        }
                    }
                    
                    foreach ($e in $events) {
                        [PSCustomObject]@{
                            TargetUser = $e.Properties[5].Value
                            LogonType  = $e.Properties[8].Value
                            SourceHost = $e.Properties[18].Value
                        }
                    }
                }
            } -AsJob
    } catch {
        Write-ScanLog "  ОШИБКА запуска заданий: $_" -Level Error
        return $results
    }

    # --- 4. Ожидание с чтением логов из дочерних задач (Real-time polling) ---
    $childJobs = $parentJob.ChildJobs
    Write-ScanLog "  Создано дочерних заданий: $($childJobs.Count)" -Level Success
    foreach ($cj in $childJobs) {
        Write-ScanLog "    -> Назначено для DC: $($cj.Location)" -Level Info
    }

    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $lastSeen = @{}
    foreach ($cj in $childJobs) { $lastSeen[$cj.Id] = 0 }
    $globalTimeout = $TimeoutSeconds + 120

    while ($true) {
        $running = @($childJobs | Where-Object { $_.State -eq 'Running' -or $_.State -eq 'NotStarted' })
        if ($running.Count -eq 0) { break }
        
        if ($sw.Elapsed.TotalSeconds -ge $globalTimeout) {
            Write-ScanLog "  ГЛОБАЛЬНЫЙ ТАЙМАУТ ($globalTimeout с) — прерываем оставшиеся задания." -Level Warning
            $running | Stop-Job -ErrorAction SilentlyContinue
            break
        }

        foreach ($cj in $running) {
            # Читаем логи конкретного контроллера домена
            $output = Receive-Job -Job $cj -Keep -ErrorAction SilentlyContinue
            for ($i = $lastSeen[$cj.Id]; $i -lt $output.Count; $i++) {
                $item = $output[$i]
                if ($item -is [string]) {
                    if ($item -match '^\[PROGRESS\]') {
                        $msg = $item -replace '^\[PROGRESS\]\s*', ''
                        Write-ScanLog "    $msg" -Level Info
                    } elseif ($item -match '^\[WARNING\]') {
                        $msg = $item -replace '^\[WARNING\]\s*', ''
                        Write-ScanLog "    $msg" -Level Warning
                    }
                }
            }
            $lastSeen[$cj.Id] = $output.Count
        }
        Start-Sleep -Milliseconds 500
    }
    $sw.Stop()

    # --- 5. Сбор финальных результатов ---
    $rawResults = @()
    foreach ($cj in $childJobs) {
        $dcName = $cj.Location
        $finalOut = @(Receive-Job -Job $cj -ErrorAction SilentlyContinue -ErrorVariable dcErr)
        $objects = @($finalOut | Where-Object { $_ -isnot [string] })
        $rawResults += $objects
        
        foreach ($e in $dcErr) { Write-ScanLog "    [$dcName] $($e.Exception.Message)" -Level Warning }
        
        if ($cj.State -eq 'Failed') {
            Write-ScanLog "    [$dcName] ЗАВЕРШИЛСЯ С ОШИБКОЙ" -Level Error
        } elseif ($cj.State -eq 'Stopped') {
            Write-ScanLog "    [$dcName] ОСТАНОВЛЕН (таймаут)" -Level Warning
        } else {
            Write-ScanLog "    [$dcName] завершен (всего событий: $($objects.Count))" -Level Success
        }
        Remove-Job -Job $cj -Force -ErrorAction SilentlyContinue
    }
    Remove-Job -Job $parentJob -Force -ErrorAction SilentlyContinue

    Write-ScanLog "  Всего выкачано событий: $($rawResults.Count) за $($sw.Elapsed.TotalSeconds.ToString('0.0'))с"

    # --- 6. Фильтрация и формирование итогового списка ---
    $validSams = [System.Collections.Generic.HashSet[string]]::new([string[]]$AccountSams, [System.StringComparer]::OrdinalIgnoreCase)
    $validEvents = @()
    
    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 '^\\\\', ''
        
        $validEvents += [PSCustomObject]@{
            AccountSam = $ev.TargetUser
            Server     = $src
            WhereUsed  = "Журнал входа DC (4624, logon type $($ev.LogonType))"
        }
        
        $results.Add($validEvents[-1])
    }

    # --- 7. ВЫВОД СВОДНОЙ СТАТИСТИКИ ПО УЧЕТНЫМ ЗАПИСЯМ ---
    Write-ScanLog "  СВОДКА ПО НАЙДЕННЫМ СОБЫТИЯМ (по учетным записям):" -Level Header
    $groupedEvents = $validEvents | Group-Object -Property AccountSam -AsHashTable -AsString
    
    foreach ($sam in $AccountSams) {
        if ($groupedEvents.ContainsKey($sam)) {
            $evs = $groupedEvents[$sam]
            $totalHits = $evs.Count
            # Получаем уникальные серверы, на которых была замечена УЗ
            $uniqueServers = @($evs | Select-Object -ExpandProperty Server -Unique)
            $uniqueSrvCount = $uniqueServers.Count
            
            # Формируем красивый список серверов (обрезаем, если он слишком длинный)
            $srvList = $uniqueServers -join ', '
            if ($srvList.Length -gt 70) { $srvList = $srvList.Substring(0, 67) + "..." }
            
            Write-ScanLog ("    {0,-25} : событий {1,-5} | серверов: {2,-3} ({3})" -f $sam, $totalHits, $uniqueSrvCount, $srvList) -Level Success
        } else {
            Write-ScanLog ("    {0,-25} : событий 0 | серверов: 0" -f $sam) -Level Info
        }
    }
    Write-Host ""

    return $results
}