Add Binoculars query diagnostics and parked note
This commit is contained in:
+185
-20
@@ -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>='$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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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, `
|
||||||
|
|||||||
Reference in New Issue
Block a user