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


function Get-DCLogonEvents {
<#
.SYNOPSIS
    Ищет события 4624 на всех DC параллельно, разбивая запрос на пакеты для обхода ограничений длины XPath.
#>
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]]$AccountSams,
        [Parameter(Mandatory)][string]$DomainName,
        [int]$Days = 30,
        [int]$MaxEventsPerDC = 100000,
        [int]$TimeoutSeconds = 300,
        [int]$ChunkSize = 20  # Количество УЗ в одном XPath-запросе
    )

    $results = New-Object System.Collections.Generic.List[object]

    if (-not $AccountSams -or $AccountSams.Count -eq 0) {
        Write-ScanLog "  Нет учётных записей — этап пропущен." -Level Warning
        return $results
    }

    # --- Список 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
    }
    if ($dcs.Count -eq 0) {
        Write-ScanLog "  DC не найдены — этап пропущен." -Level Warning
        return $results
    }
    Write-ScanLog "  Найдено DC ($($dcs.Count)): $($dcs -join ', ')" -Level Success

    # --- Построение XPath (нарезка на чанки) ---
    $startTimeUtc = (Get-Date).AddDays(-$Days).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ss.fffZ")
    
    $logonDataValues = (@('3','4','5','8','9','10') | ForEach-Object { "Data='$_'" }) -join ' or '
    $logonCondition  = "Data[@Name='LogonType' and ($logonDataValues)]"

    $xpathQueries = @()
    
    # Нарезаем массив учетных записей на части по $ChunkSize
    for ($i = 0; $i -lt $AccountSams.Count; $i += $ChunkSize) {
        $endIndex = [Math]::Min($i + $ChunkSize - 1, $AccountSams.Count - 1)
        $chunk = $AccountSams[$i..$endIndex]
        
        # В XPath одинарная кавычка экранируется удвоением (''), 
        # а не через SecurityElement (который делает невалидный &apos;)
        $escapedUsers = $chunk | ForEach-Object { $_.Replace("'", "''") }
        $userDataValues = ($escapedUsers | ForEach-Object { "Data='$_'" }) -join ' or '
        $userCondition  = "Data[@Name='TargetUserName' and ($userDataValues)]"
        
        $xpath = "*[System[(EventID=4624) and TimeCreated[@SystemTime>='$startTimeUtc']] " +
                 "and EventData[$userCondition and $logonCondition]]"
        $xpathQueries += $xpath
    }

    Write-ScanLog "  Сформировано $($xpathQueries.Count) XPath-запросов (по $ChunkSize УЗ в каждом)"
    Write-ScanLog "  Параллельный опрос $($dcs.Count) DC (таймаут ${TimeoutSeconds}с)..."

    # --- PSSessionOption с таймаутами (в миллисекундах) ---
    $sessionOpt = New-PSSessionOption `
        -OpenTimeout        ($TimeoutSeconds * 1000) `
        -OperationTimeout   ($TimeoutSeconds * 1000) `
        -CancelTimeout      ($TimeoutSeconds * 1000) `
        -IdleTimeout        ($TimeoutSeconds * 2000)

    # --- Запуск параллельно на всех DC ---
    $sw = [System.Diagnostics.Stopwatch]::StartNew()
    $jobs = @()
    try {
        # Используем $using: для безопасной передачи локальных массивов в удаленную сессию
        $jobs = @(Invoke-Command -ComputerName $dcs -SessionOption $sessionOpt -AsJob -ScriptBlock {
            $maxPerDc = $using:MaxEventsPerDC
            $xpaths   = $using:xpathQueries

            $fetchedCount = 0
            $returnEvents = @()
            
            foreach ($xpath in $xpaths) {
                if ($fetchedCount -ge $maxPerDc) { break }
                
                $remaining = $maxPerDc - $fetchedCount
                try {
                    $events = Get-WinEvent -LogName 'Security' -FilterXPath $xpath `
                                            -MaxEvents $remaining -ErrorAction Stop
                    
                    if ($events) {
                        $returnEvents += @($events)
                        $fetchedCount += @($events).Count
                    }
                } catch {
                    if ($_.FullyQualifiedErrorId -match 'NoMatchingEventsFound') { continue }
                    Write-Warning "[$env:COMPUTERNAME] Ошибка XPath: $($_.Exception.Message)"
                }
            }
            
            # Оставляем только нужные поля для экономии трафика
            $returnEvents | ForEach-Object {
                [PSCustomObject]@{
                    TargetUser = $_.Properties[5].Value
                    LogonType  = $_.Properties[8].Value
                    SourceHost = $_.Properties[18].Value
                }
            }
        })
    } catch {
        Write-ScanLog "  ОШИБКА запуска заданий: $_" -Level Error
        $sw.Stop()
        return $results
    }

    # --- Глобальный дедлайн ---
    $globalTimeout = $TimeoutSeconds + 120
    $null = Wait-Job -Job $jobs -Timeout $globalTimeout | Out-Null
    $sw.Stop()

    # --- Сбор результатов ---
    $rawResults = @()
    foreach ($job in $jobs) {
        $dcName = if ($job.Name -match 'DC\s+(.+)$') { $Matches[1] } else { $job.Name }
        if ($job.State -eq 'Completed') {
            $dcOut = @(Receive-Job -Job $job -ErrorAction SilentlyContinue -ErrorVariable dcErr)
            Write-ScanLog "    $dcName : событий $($dcOut.Count) (готово)" -Level Success
            $rawResults += $dcOut
            foreach ($e in $dcErr) {
                Write-ScanLog "    $dcName : $($e.Exception.Message)" -Level Warning
            }
        } elseif ($job.State -eq 'Running') {
            Write-ScanLog "    $dcName : ТАЙМАУТ ($globalTimeout с) — пропущен" -Level Warning
        } else {
            Write-ScanLog "    $dcName : состояние $($job.State)" -Level Warning
        }
        Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
    }

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

    # --- Преобразование в итоговые записи ---
    $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
}