Загрузка данных
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. Подготовка базовых параметров для передачи ---
$startTimeUtc = [DateTime]::UtcNow.AddDays(-$Days).ToString("yyyy-MM-ddTHH:mm:ss.fffZ", [System.Globalization.CultureInfo]::InvariantCulture)
Write-ScanLog " Отправка фоновых заданий на $($dcs.Count) DC..." -Level Header
Write-ScanLog " Период: с $startTimeUtc (UTC), пакетов: $( [Math]::Ceiling($AccountSams.Count / $BatchSize) ), по $BatchSize УЗ." -Level Info
# --- 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 (,$AccountSams), $startTimeUtc, $BatchSize, $MaxEventsPerDC `
-ScriptBlock {
param($accounts, $startUtc, $batchSize, $max)
$batchNum = 0
$totalBatches = [Math]::Ceiling($accounts.Count / $batchSize)
$dcName = $env:COMPUTERNAME
Write-Output "[PROGRESS] [$dcName] Начало обработки $totalBatches пакетов (XML)"
# Нарезаем массив прямо на сервере
for ($i = 0; $i -lt $accounts.Count; $i += $batchSize) {
$batchNum++
$endIdx = [Math]::Min($i + $batchSize - 1, $accounts.Count - 1)
$batch = $accounts[$i..$endIdx]
$accList = $batch -join ', '
if ($accList.Length -gt 60) { $accList = $accList.Substring(0, 57) + "..." }
Write-Output "[PROGRESS] [$dcName] Запрос пакета $batchNum/$totalBatches (УЗ: $accList)"
try {
# Генерируем XML прямо здесь
$escapedUsers = $batch | ForEach-Object { [System.Security.SecurityElement]::Escape($_) }
$userCond = ($escapedUsers | ForEach-Object { ".='$_'" }) -join ' or '
$userCond = "Data[@Name='TargetUserName' and ($userCond)]"
$xmlString = @"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">
*[System[(EventID=4624) and TimeCreated[@SystemTime>='$startUtc']]]
and
*[EventData[$userCond]]
</Select>
</Query>
</QueryList>
"@
# Убираем лишние переносы, чтобы не нагружать парсер
$xmlString = $xmlString -replace '\r?\n', ''
# Явно приводим к XML
[xml]$xmlDoc = $xmlString
$events = @(Get-WinEvent -FilterXml $xmlDoc -MaxEvents $max -ErrorAction Stop)
Write-Output "[PROGRESS] [$dcName] Пакет $batchNum/$totalBatches - найдено событий: $($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/$totalBatches - событий нет (по фильтру)"
} else {
Write-Output "[ERROR] [$dcName] Пакет $batchNum/$totalBatches - ОШИБКА: $($_.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. ВЫВОД СВОДНОЙ СТАТИСТИКИ ---
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
}