Загрузка данных
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
}