Add Binoculars query diagnostics and parked note
This commit is contained in:
+194
-29
@@ -7,6 +7,11 @@ param(
|
||||
[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:PollOverlapSeconds = 120
|
||||
$script:DefaultThrottleLimit = 8
|
||||
@@ -1030,37 +1035,172 @@ function Invoke-BinocularsParallelEventQuery {
|
||||
[int[]]$EventIds
|
||||
)
|
||||
|
||||
try {
|
||||
$emitRow = {
|
||||
param(
|
||||
[Parameter(Mandatory)]
|
||||
$EventRecord,
|
||||
[Parameter(Mandatory)]
|
||||
[string]$QueryStrategy
|
||||
)
|
||||
|
||||
$xmlText = $null
|
||||
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 = @{
|
||||
LogName = 'Security'
|
||||
ID = $EventIds
|
||||
ID = $selectedIds
|
||||
StartTime = $StartTime
|
||||
EndTime = $EndTime
|
||||
}
|
||||
|
||||
Get-WinEvent -ComputerName $ComputerName -FilterHashtable $filter -ErrorAction Stop |
|
||||
ForEach-Object {
|
||||
[pscustomobject]@{
|
||||
ComputerName = $ComputerName
|
||||
Id = [int]$_.Id
|
||||
RecordId = [long]$_.RecordId
|
||||
TimeCreated = $_.TimeCreated
|
||||
Xml = $_.ToXml()
|
||||
Message = $_.Message
|
||||
Error = $null
|
||||
}
|
||||
}
|
||||
Get-WinEvent -ComputerName $ComputerName -FilterHashtable $filter -ErrorAction Stop
|
||||
}
|
||||
catch {
|
||||
[pscustomobject]@{
|
||||
ComputerName = $ComputerName
|
||||
Id = $null
|
||||
RecordId = $null
|
||||
TimeCreated = $null
|
||||
Xml = $null
|
||||
Message = $null
|
||||
Error = $_.Exception.Message
|
||||
if ($hashtableResult.Error) {
|
||||
$diagnostics.Add("FilterHashtable failed: $($hashtableResult.Error)") | Out-Null
|
||||
}
|
||||
elseif ($hashtableResult.Rows.Count -gt 0) {
|
||||
return @($hashtableResult.Rows)
|
||||
}
|
||||
else {
|
||||
$diagnostics.Add('FilterHashtable returned 0 rows.') | Out-Null
|
||||
}
|
||||
|
||||
$filterXmlText = @"
|
||||
<QueryList>
|
||||
<Query Id="0" Path="Security">
|
||||
<Select Path="Security">*[System[(($idPredicates)) and TimeCreated[@SystemTime>='$startUtcText' and @SystemTime<='$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
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($broadResult.Error) {
|
||||
$diagnostics.Add("TimeWindowLocalIdFilter failed: $($broadResult.Error)") | Out-Null
|
||||
return [pscustomobject]@{
|
||||
ComputerName = $ComputerName
|
||||
Id = $null
|
||||
RecordId = $null
|
||||
TimeCreated = $null
|
||||
Xml = $null
|
||||
Message = $null
|
||||
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)
|
||||
|
||||
@@ -1095,22 +1235,36 @@ function Invoke-BinocularsParallelEventQuery {
|
||||
try {
|
||||
$jobResults = $job.PowerShell.EndInvoke($job.Handle)
|
||||
$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]@{
|
||||
DomainController = $job.DomainController
|
||||
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 {
|
||||
$successfulDomainControllers.Add($job.DomainController) | Out-Null
|
||||
foreach ($row in $jobRows) {
|
||||
foreach ($row in $eventRows) {
|
||||
$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 {
|
||||
@@ -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
|
||||
|
||||
$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)
|
||||
$records = New-Object System.Collections.Generic.List[object]
|
||||
|
||||
@@ -1221,6 +1376,13 @@ function Invoke-BinocularsSearch {
|
||||
}
|
||||
|
||||
$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) {
|
||||
Write-BinocularsStatus -Message 'Search cancelled before all DC queries finished.' -StatusTextBox $StatusTextBox
|
||||
}
|
||||
@@ -1232,6 +1394,7 @@ function Invoke-BinocularsSearch {
|
||||
SuccessfulDomainControllers = @($rawQuery.SuccessfulDomainControllers)
|
||||
DomainControllers = @($domainControllers)
|
||||
EventIds = @($EventIds | Sort-Object -Unique)
|
||||
RawEventCount = $candidateCount
|
||||
Identity = $Identity
|
||||
StartTime = $StartTime
|
||||
EndTime = $EndTime
|
||||
@@ -1934,8 +2097,9 @@ function Show-BinocularsGui {
|
||||
}
|
||||
$pdcText = if ($pdcEntry) { $pdcEntry[0].HostName } else { 'N/A' }
|
||||
$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, `
|
||||
$filterText, `
|
||||
$searchedDcCount, `
|
||||
@@ -1943,6 +2107,7 @@ function Show-BinocularsGui {
|
||||
$unreachableCount, `
|
||||
$unknownCount, `
|
||||
$pdcText, `
|
||||
$rawEventCount, `
|
||||
$records.Count, `
|
||||
$successCount, `
|
||||
$failureCount, `
|
||||
|
||||
Reference in New Issue
Block a user