Загрузка данных
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 (который делает невалидный ')
$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
}