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


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. Генерация XML-пакетов с корректным синтаксисом XPath ---
    $startTimeUtc = [DateTime]::UtcNow.AddDays(-$Days).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", [System.Globalization.CultureInfo]::InvariantCulture)

    $xmlQueries = @()
    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($_) }
        # Исправленный синтаксис: Data[@Name='...' and (.='user1' or .='user2')]
        $userCond = ($escapedUsers | ForEach-Object { ".='$_'" }) -join ' or '
        $userCond = "Data[@Name='TargetUserName' and ($userCond)]"
        
        $xmlQuery = @"
<QueryList>
  <Query Id="0" Path="Security">
    <Select Path="Security">
      *[System[(EventID=4624) and TimeCreated[@SystemTime&gt;='$startTimeUtc']]]
      and
      *[EventData[$userCond]]
    </Select>
  </Query>
</QueryList>
"@
                 
        $xmlQueries += [PSCustomObject]@{
            XmlQuery = $xmlQuery
            Accounts = $batch
        }
    }

    Write-ScanLog "  Сгенерировано $($xmlQueries.Count) XML-пакетов (по $BatchSize УЗ в каждом)."
    Write-ScanLog "  Пример XML-запроса для первого пакета:" -Level Info
    Write-Host "    $($xmlQueries[0].XmlQuery -replace '\s+', ' ')" -ForegroundColor DarkGray
    Write-ScanLog "  Отправка фоновых заданий на $($dcs.Count) DC..." -Level Header

    # --- 3. Запуск заданий ---
    $sessionOpt = New-PSSessionOption `
        -OpenTimeout      ($TimeoutSeconds * 1000) `
        -OperationTimeout ($TimeoutSeconds * 1000) `
        -CancelTimeout    ($TimeoutSeconds * 1000) `
        -IdleTimeout      ($TimeoutSeconds * 2000)

    $jobs = @()
    foreach ($dc in $dcs) {
        Write-ScanLog "  Создание задания для DC: $dc ..." -Level Info
        try {
            $job = Invoke-Command -ComputerName $dc -SessionOption $sessionOpt `
                -ArgumentList (,$xmlQueries), $MaxEventsPerDC `
                -ScriptBlock {
                    param($queries, $max)
                    $batchNum = 0
                    $total = $queries.Count
                    $dcName = $env:COMPUTERNAME
                    
                    Write-Output "[PROGRESS] [$dcName] Начало обработки $total пакетов (XML)"
                    
                    foreach ($item in $queries) {
                        $batchNum++
                        $accList = $item.Accounts -join ', '
                        if ($accList.Length -gt 60) { $accList = $accList.Substring(0, 57) + "..." }
                        
                        Write-Output "[PROGRESS] [$dcName] Запрос пакета $batchNum/$total (УЗ: $accList)"
                        
                        try {
                            # Явное приведение к XmlDocument на случай проблем с десериализацией
                            $xmlDoc = [xml]$item.XmlQuery
                            $events = @(Get-WinEvent -FilterXml $xmlDoc -MaxEvents $max -ErrorAction Stop)
                            Write-Output "[PROGRESS] [$dcName] Пакет $batchNum/$total - найдено событий: $($events.Count)"
                            
                            foreach ($e in $events) {
                                $logonType = $e.Properties[8].Value
                                if ($logonType -in @('3','4','5','8','9','10')) {
                                    [PSCustomObject]@{
                                        TargetUser = $e.Properties[5].Value
                                        LogonType  = $logonType
                                        SourceHost = $e.Properties[18].Value
                                    }
                                }
                            }
                        } catch {
                            if ($_.FullyQualifiedErrorId -match 'NoMatchingEventsFound') {
                                Write-Output "[PROGRESS] [$dcName] Пакет $batchNum/$total - событий нет (по фильтру)"
                            } else {
                                Write-Output "[ERROR] [$dcName] Пакет $batchNum/$total - ОШИБКА: $($_.Exception.Message)"
                            }
                        }
                    }
                } -AsJob
            $jobs += $job
            Write-ScanLog "    Задание создано: ID=$($job.Id), State=$($job.State)" -Level Success
        } catch {
            Write-ScanLog "    ОШИБКА создания задания для $dc : $_" -Level Error
        }
    }

    if ($jobs.Count -eq 0) { return $results }

    # --- 4. Ожидание с чтением логов (без изменений) ---
    $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' -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 ($job in $running) {
            $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 "    $($item -replace '^\[PROGRESS\]\s*', '')" -Level Info }
                    elseif ($item -match '^\[ERROR\]') { Write-ScanLog "    $($item -replace '^\[ERROR\]\s*', '')" -Level Error }
                }
            }
            $lastSeen[$job.Id] = $output.Count
        }
        Start-Sleep -Milliseconds 500
    }
    $sw.Stop()

    # --- 5. Сбор финальных результатов (без изменений) ---
    $rawResults = @()
    foreach ($job in $jobs) {
        $dcName = $job.Location; if (-not $dcName) { $dcName = "Unknown" }
        $finalOut = @(Receive-Job -Job $job -ErrorAction SilentlyContinue -ErrorVariable dcErr)
        foreach ($item in $finalOut) {
            if ($item -is [string]) {
                if ($item -match '^\[PROGRESS\]') { Write-ScanLog "    $($item -replace '^\[PROGRESS\]\s*', '')" -Level Info }
                elseif ($item -match '^\[ERROR\]') { Write-ScanLog "    $($item -replace '^\[ERROR\]\s*', '')" -Level Error }
            }
        }
        $objects = @($finalOut | Where-Object { $_ -isnot [string] })
        $rawResults += $objects
        if ($job.State -eq 'Failed') {
            Write-ScanLog "    [$dcName] ЗАВЕРШИЛСЯ С ОШИБКОЙ!" -Level Error
            foreach ($err in $job.Error) { Write-ScanLog "      Причина: $($err.Exception.Message)" -Level Error }
        } else {
            Write-ScanLog "    [$dcName] завершен (событий: $($objects.Count))" -Level Success
        }
        Remove-Job -Job $job -Force -ErrorAction SilentlyContinue
    }

    # --- 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. ВЫВОД СВОДНОЙ СТАТИСТИКИ (с защитой от $null) ---
    Write-ScanLog "  СВОДКА ПО НАЙДЕННЫМ СОБЫТИЯМ (по учетным записям):" -Level Header
    $groupedEvents = @{}
    if ($validEvents.Count -gt 0) {
        $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
}