Add Binoculars query diagnostics and parked note

This commit is contained in:
2026-04-15 14:46:03 +12:00
parent 0af5a87a88
commit 747e519760
+185 -20
View File
@@ -7,6 +7,11 @@ param(
[string]$ExportPath [string]$ExportPath
) )
# Note [2026-04-15]:
# Binoculars auth collection is still not returning usable events in the target environment.
# The current build includes fallback DC query strategies and extra diagnostics, but the issue is parked for follow-up.
# Start the next round with All Users + Lockouts and Failures and review the status pane candidate counts per DC.
$script:RelevantEventIds = @(4624, 4625, 4634, 4647, 4740, 4767, 4768, 4769, 4770, 4771, 4776) $script:RelevantEventIds = @(4624, 4625, 4634, 4647, 4740, 4767, 4768, 4769, 4770, 4771, 4776)
$script:PollOverlapSeconds = 120 $script:PollOverlapSeconds = 120
$script:DefaultThrottleLimit = 8 $script:DefaultThrottleLimit = 8
@@ -1030,38 +1035,173 @@ function Invoke-BinocularsParallelEventQuery {
[int[]]$EventIds [int[]]$EventIds
) )
$emitRow = {
param(
[Parameter(Mandatory)]
$EventRecord,
[Parameter(Mandatory)]
[string]$QueryStrategy
)
$xmlText = $null
try { try {
$xmlText = $EventRecord.ToXml()
}
catch {
$xmlText = $null
}
$messageText = $null
try {
$messageText = $EventRecord.FormatDescription()
}
catch {
try {
$messageText = $EventRecord.Message
}
catch {
$messageText = $null
}
}
return [pscustomobject]@{
ComputerName = $ComputerName
Id = [int]$EventRecord.Id
RecordId = [long]$EventRecord.RecordId
TimeCreated = $EventRecord.TimeCreated
Xml = $xmlText
Message = $messageText
QueryStrategy = $QueryStrategy
Diagnostics = $null
IsMeta = $false
Error = $null
}
}
$executeStrategy = {
param(
[Parameter(Mandatory)]
[string]$StrategyName,
[Parameter(Mandatory)]
[scriptblock]$Body
)
$rows = New-Object System.Collections.Generic.List[object]
try {
foreach ($eventRecord in @(& $Body)) {
if ($null -eq $eventRecord) {
continue
}
$rows.Add((& $emitRow -EventRecord $eventRecord -QueryStrategy $StrategyName)) | Out-Null
}
return [pscustomobject]@{
Strategy = $StrategyName
Rows = @($rows)
Error = $null
}
}
catch {
return [pscustomobject]@{
Strategy = $StrategyName
Rows = @()
Error = $_.Exception.Message
}
}
}
$diagnostics = New-Object System.Collections.Generic.List[string]
$selectedIds = @($EventIds | Sort-Object -Unique)
$startUtcText = $StartTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
$endUtcText = $EndTime.ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
$idPredicates = (($selectedIds | ForEach-Object { "EventID=$_"} ) -join ' or ')
$hashtableResult = & $executeStrategy -StrategyName 'FilterHashtable' -Body {
$filter = @{ $filter = @{
LogName = 'Security' LogName = 'Security'
ID = $EventIds ID = $selectedIds
StartTime = $StartTime StartTime = $StartTime
EndTime = $EndTime EndTime = $EndTime
} }
Get-WinEvent -ComputerName $ComputerName -FilterHashtable $filter -ErrorAction Stop | Get-WinEvent -ComputerName $ComputerName -FilterHashtable $filter -ErrorAction Stop
ForEach-Object { }
[pscustomobject]@{ if ($hashtableResult.Error) {
ComputerName = $ComputerName $diagnostics.Add("FilterHashtable failed: $($hashtableResult.Error)") | Out-Null
Id = [int]$_.Id }
RecordId = [long]$_.RecordId elseif ($hashtableResult.Rows.Count -gt 0) {
TimeCreated = $_.TimeCreated return @($hashtableResult.Rows)
Xml = $_.ToXml() }
Message = $_.Message else {
Error = $null $diagnostics.Add('FilterHashtable returned 0 rows.') | Out-Null
}
$filterXmlText = @"
<QueryList>
<Query Id="0" Path="Security">
<Select Path="Security">*[System[(($idPredicates)) and TimeCreated[@SystemTime&gt;='$startUtcText' and @SystemTime&lt;='$endUtcText']]]</Select>
</Query>
</QueryList>
"@
$filterXmlResult = & $executeStrategy -StrategyName 'FilterXml' -Body {
Get-WinEvent -ComputerName $ComputerName -FilterXml ([xml]$filterXmlText) -ErrorAction Stop
}
if ($filterXmlResult.Error) {
$diagnostics.Add("FilterXml failed: $($filterXmlResult.Error)") | Out-Null
}
elseif ($filterXmlResult.Rows.Count -gt 0) {
return @($filterXmlResult.Rows)
}
else {
$diagnostics.Add('FilterXml returned 0 rows.') | Out-Null
}
$broadResult = & $executeStrategy -StrategyName 'TimeWindowLocalIdFilter' -Body {
$broadFilter = @{
LogName = 'Security'
StartTime = $StartTime
EndTime = $EndTime
}
foreach ($eventRecord in @(Get-WinEvent -ComputerName $ComputerName -FilterHashtable $broadFilter -ErrorAction Stop)) {
if ([int]$eventRecord.Id -in $selectedIds) {
$eventRecord
} }
} }
} }
catch { if ($broadResult.Error) {
[pscustomobject]@{ $diagnostics.Add("TimeWindowLocalIdFilter failed: $($broadResult.Error)") | Out-Null
return [pscustomobject]@{
ComputerName = $ComputerName ComputerName = $ComputerName
Id = $null Id = $null
RecordId = $null RecordId = $null
TimeCreated = $null TimeCreated = $null
Xml = $null Xml = $null
Message = $null Message = $null
Error = $_.Exception.Message QueryStrategy = 'Error'
Diagnostics = ($diagnostics -join ' ')
IsMeta = $true
Error = $broadResult.Error
} }
} }
if ($broadResult.Rows.Count -gt 0) {
return @($broadResult.Rows)
}
$diagnostics.Add('TimeWindowLocalIdFilter returned 0 rows.') | Out-Null
return [pscustomobject]@{
ComputerName = $ComputerName
Id = $null
RecordId = $null
TimeCreated = $null
Xml = $null
Message = $null
QueryStrategy = 'NoResults'
Diagnostics = ($diagnostics -join ' ')
IsMeta = $true
Error = $null
}
}).AddArgument($domainController).AddArgument($StartTime).AddArgument($EndTime).AddArgument($EventIds) }).AddArgument($domainController).AddArgument($StartTime).AddArgument($EndTime).AddArgument($EventIds)
$jobs += [pscustomobject]@{ $jobs += [pscustomobject]@{
@@ -1095,22 +1235,36 @@ function Invoke-BinocularsParallelEventQuery {
try { try {
$jobResults = $job.PowerShell.EndInvoke($job.Handle) $jobResults = $job.PowerShell.EndInvoke($job.Handle)
$jobRows = @($jobResults) $jobRows = @($jobResults)
$errorRows = @($jobRows | Where-Object { $_.Error })
$eventRows = @($jobRows | Where-Object { -not $_.Error -and -not $_.IsMeta })
$metaRows = @($jobRows | Where-Object { $_.IsMeta })
if ($jobRows.Count -eq 1 -and $jobRows[0].Error) { if ($errorRows.Count -gt 0 -and $eventRows.Count -eq 0) {
$errorRow = @($errorRows | Select-Object -First 1)[0]
$errors.Add([pscustomobject]@{ $errors.Add([pscustomobject]@{
DomainController = $job.DomainController DomainController = $job.DomainController
ErrorType = 'Query' ErrorType = 'Query'
Message = $jobRows[0].Error Message = $errorRow.Error
}) })
Write-BinocularsStatus -Message "Failed to query $($job.DomainController): $($jobRows[0].Error)" -StatusTextBox $StatusTextBox Write-BinocularsStatus -Message "Failed to query $($job.DomainController): $($errorRow.Error)" -StatusTextBox $StatusTextBox
} }
else { else {
$successfulDomainControllers.Add($job.DomainController) | Out-Null $successfulDomainControllers.Add($job.DomainController) | Out-Null
foreach ($row in $jobRows) { foreach ($row in $eventRows) {
$rawEvents.Add($row) $rawEvents.Add($row)
} }
Write-BinocularsStatus -Message "Queried $($job.DomainController): $($jobRows.Count) candidate events returned." -StatusTextBox $StatusTextBox if ($eventRows.Count -gt 0) {
$strategyName = @($eventRows | Select-Object -First 1 -ExpandProperty QueryStrategy)
Write-BinocularsStatus -Message "Queried $($job.DomainController): $($eventRows.Count) candidate events returned via $strategyName." -StatusTextBox $StatusTextBox
}
else {
$diagnosticsText = @($metaRows | Select-Object -First 1 -ExpandProperty Diagnostics)
if ([string]::IsNullOrWhiteSpace($diagnosticsText)) {
$diagnosticsText = 'All query strategies returned 0 rows.'
}
Write-BinocularsStatus -Message "Queried $($job.DomainController): 0 candidate events returned. $diagnosticsText" -StatusTextBox $StatusTextBox
}
} }
} }
catch { catch {
@@ -1197,6 +1351,7 @@ function Invoke-BinocularsSearch {
Write-BinocularsStatus -Message "Query window: $($StartTime.ToString('yyyy-MM-dd HH:mm:ss')) to $($EndTime.ToString('yyyy-MM-dd HH:mm:ss'))." -StatusTextBox $StatusTextBox Write-BinocularsStatus -Message "Query window: $($StartTime.ToString('yyyy-MM-dd HH:mm:ss')) to $($EndTime.ToString('yyyy-MM-dd HH:mm:ss'))." -StatusTextBox $StatusTextBox
$rawQuery = Invoke-BinocularsParallelEventQuery -DomainControllers $domainControllers -StartTime $StartTime -EndTime $EndTime -EventIds $EventIds -StatusTextBox $StatusTextBox $rawQuery = Invoke-BinocularsParallelEventQuery -DomainControllers $domainControllers -StartTime $StartTime -EndTime $EndTime -EventIds $EventIds -StatusTextBox $StatusTextBox
$candidateCount = @($rawQuery.RawEvents).Count
$seenFingerprints = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase) $seenFingerprints = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)
$records = New-Object System.Collections.Generic.List[object] $records = New-Object System.Collections.Generic.List[object]
@@ -1221,6 +1376,13 @@ function Invoke-BinocularsSearch {
} }
$sortedRecords = $records | Sort-Object -Property TimeCreated -Descending $sortedRecords = $records | Sort-Object -Property TimeCreated -Descending
if ($candidateCount -eq 0) {
Write-BinocularsStatus -Message 'No candidate events were returned from the DCs before user matching. This usually points to event selection, time window, or auditing on the DCs.' -StatusTextBox $StatusTextBox
}
elseif (-not $Identity.SearchAllUsers -and $sortedRecords.Count -eq 0) {
Write-BinocularsStatus -Message "DCs returned $candidateCount candidate events, but none matched the current user filter." -StatusTextBox $StatusTextBox
}
if ($rawQuery.Cancelled) { if ($rawQuery.Cancelled) {
Write-BinocularsStatus -Message 'Search cancelled before all DC queries finished.' -StatusTextBox $StatusTextBox Write-BinocularsStatus -Message 'Search cancelled before all DC queries finished.' -StatusTextBox $StatusTextBox
} }
@@ -1232,6 +1394,7 @@ function Invoke-BinocularsSearch {
SuccessfulDomainControllers = @($rawQuery.SuccessfulDomainControllers) SuccessfulDomainControllers = @($rawQuery.SuccessfulDomainControllers)
DomainControllers = @($domainControllers) DomainControllers = @($domainControllers)
EventIds = @($EventIds | Sort-Object -Unique) EventIds = @($EventIds | Sort-Object -Unique)
RawEventCount = $candidateCount
Identity = $Identity Identity = $Identity
StartTime = $StartTime StartTime = $StartTime
EndTime = $EndTime EndTime = $EndTime
@@ -1934,8 +2097,9 @@ function Show-BinocularsGui {
} }
$pdcText = if ($pdcEntry) { $pdcEntry[0].HostName } else { 'N/A' } $pdcText = if ($pdcEntry) { $pdcEntry[0].HostName } else { 'N/A' }
$searchedDcCount = if ($Result) { $Result.DomainControllers.Count } else { $reachableCount } $searchedDcCount = if ($Result) { $Result.DomainControllers.Count } else { $reachableCount }
$rawEventCount = if ($Result) { [int]$Result.RawEventCount } else { 0 }
$summaryLabel.Text = 'User: {0} | Filter: {1} | Search DCs: {2} | Green: {3} | Red: {4} | Unknown: {5} | PDC: {6} | Events: {7} | Success: {8} | Failure: {9} | Lockout: {10} | Unlock: {11} | Logoff: {12} | Errors: {13} | Window: {14}' -f ` $summaryLabel.Text = 'User: {0} | Filter: {1} | Search DCs: {2} | Green: {3} | Red: {4} | Unknown: {5} | PDC: {6} | Candidates: {7} | Events: {8} | Success: {9} | Failure: {10} | Lockout: {11} | Unlock: {12} | Logoff: {13} | Errors: {14} | Window: {15}' -f `
$resolvedUser, ` $resolvedUser, `
$filterText, ` $filterText, `
$searchedDcCount, ` $searchedDcCount, `
@@ -1943,6 +2107,7 @@ function Show-BinocularsGui {
$unreachableCount, ` $unreachableCount, `
$unknownCount, ` $unknownCount, `
$pdcText, ` $pdcText, `
$rawEventCount, `
$records.Count, ` $records.Count, `
$successCount, ` $successCount, `
$failureCount, ` $failureCount, `