Created
December 2, 2025 21:46
-
-
Save SweetAsNZ/738965ad651eb2bcf9951c2a6c3c44bd to your computer and use it in GitHub Desktop.
Analyzes Windows Firewall logs of the local machine and another computer(s) to identify network traffic that is sent by a Windows Computer with no matching inbound traffic on the destination computer.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function Compare-WindowsFirewallLogs { | |
| <# | |
| .SYNOPSIS | |
| Analyzes Windows Firewall logs of the local machine and another computer(s) to identify network traffic that is sent by a | |
| Computer with no matching inbound traffic on the destination computer. | |
| .DESCRIPTION | |
| This function analyzes outbound traffic from the local computer and checks if the destination | |
| computer(s) received that traffic. It identifies network communication failures, blocked connections, | |
| or dropped packets by finding outbound SEND traffic that has no corresponding inbound RECEIVE traffic | |
| on the destination server. | |
| The function is useful for troubleshooting network connectivity issues, firewall rule problems, | |
| and identifying traffic that may be blocked between servers. It matches based on source IP, | |
| destination IP, destination port, and protocol. | |
| .PARAMETER FWLog | |
| The specific firewall log file to read. Options are 'Domainfw.log', 'Firewall.log', | |
| 'Privatefw.log', and 'Publicfw.log'. Default is 'Domainfw.log'. | |
| .PARAMETER OtherComputers | |
| An array of remote computer names to retrieve firewall logs from for comparison with the local log. | |
| .PARAMETER ForLastHours | |
| Number of hours to look back from the current time for log entries. Ignored if ForLastMins is specified. | |
| .PARAMETER ForLastMins | |
| Number of minutes to look back from the current time for log entries. Takes precedence over ForLastHours. | |
| .PARAMETER Ports | |
| Optional array of specific ports to filter. Only connections using these ports will be compared. | |
| .PARAMETER CheckIPv6 | |
| When specified, includes IPv6 traffic in the analysis. By default, only IPv4 traffic is analyzed. | |
| .PARAMETER ExcludeLocalHost | |
| When specified, excludes all entries with source or destination IP of 127.0.0.1 or ::1. | |
| .PARAMETER ShowUnmatched | |
| When specified, also displays successful connections that were received on destination computers. | |
| .EXAMPLE | |
| Compare-WindowsFirewallLogs -OtherComputers 'APSE2AEX812','AKL0EX804' -ForLastHours 1 | |
| Finds outbound traffic from this computer that was not received by the remote servers in the last hour. | |
| .EXAMPLE | |
| Compare-WindowsFirewallLogs -OtherComputers 'Server1' -ForLastMins 30 -Ports 445 | |
| Checks if SMB traffic (port 445) sent to Server1 in the last 30 minutes was received. | |
| .EXAMPLE | |
| Compare-WindowsFirewallLogs -ForLastMins 30 -Action Allow -Ports 443,445 | |
| Compares allowed traffic on ports 443 and 445 for the last 30 minutes. | |
| .EXAMPLE | |
| Compare-WindowsFirewallLogs -FWLog 'Publicfw.log' -ForLastHours 2 -ShowUnmatched | |
| Analyzes public firewall log and shows both matched and unmatched connections. | |
| .EXAMPLE | |
| Compare-WindowsFirewallLogs -ForLastMins 15 -Action Drop -ExcludeLocalHost | |
| Compares dropped traffic excluding localhost connections for the last 15 minutes. | |
| .NOTES | |
| Author: Tim West | |
| Company: Air New Zealand/Sweet As Chocolate Ltd | |
| Created: 2025-12-02 | |
| Updated: 2025-12-02 | |
| Status: WIP | |
| Version: 2.0.0 | |
| .CHANGELOG | |
| 2025-12-02: Major rewrite (v2.0.0) by Tim West | |
| - Refocused script to identify blocked/unreceived traffic from local computer to remote computers | |
| - Removed local inbound traffic analysis (not needed for troubleshooting blocked traffic) | |
| - Now only analyzes remote computer inbound logs to find missing traffic | |
| - Inverted display logic: primary focus on blocked/unreceived connections (previously "unmatched") | |
| - Added local IP detection to match outbound source with remote inbound destination | |
| - Updated all output messages and statistics to reflect blocked/dropped traffic focus | |
| 2025-12-02: Initial version created by Tim West | |
| - Implemented comparison logic for outbound/inbound traffic matching | |
| - Added time-based filtering (hours and minutes) | |
| - Added action filtering (Drop, Allow, All) | |
| - Added port filtering capability | |
| - Added ExcludeLocalHost option | |
| - Added ShowUnmatched option for unmatched outbound connections | |
| - Implemented color-coded output for matched/unmatched connections | |
| - Added summary statistics for matched and unmatched connections | |
| - Added OtherComputers parameter to retrieve remote firewall logs via Get-ChildItem | |
| - Added CheckIPv6 switch parameter to include IPv6 traffic (excluded by default) | |
| - Always excludes localhost traffic (127.0.0.1, ::1) regardless of other parameters | |
| - Performance optimization: Pre-filter log lines by date before detailed parsing when time range specified | |
| #> | |
| [CmdletBinding()] | |
| param( | |
| [ValidateSet('Domainfw.log','Firewall.log','Privatefw.log','Publicfw.log')] | |
| [string]$FWLog = 'Domainfw.log', | |
| [string[]]$OtherComputers, | |
| [int]$ForLastHours, | |
| [int]$ForLastMins, | |
| [ValidateSet('Drop','Allow','All')] | |
| [string]$Action = 'All', | |
| [int[]]$Ports, | |
| [switch]$CheckIPv6, | |
| [switch]$ExcludeLocalHost, | |
| [switch]$ShowUnmatched | |
| ) | |
| # Resolve local log path | |
| $basePath = Join-Path $env:SystemRoot 'System32\LogFiles\Firewall' | |
| $logPath = Join-Path $basePath $FWLog | |
| # Validate local log file exists | |
| if (-not (Test-Path $logPath)) { | |
| Write-Host "ERROR: Local firewall log not found: $logPath" -ForegroundColor Red | |
| return | |
| } | |
| # Collect remote logs if OtherComputers specified | |
| $remoteLogs = @() | |
| if ($OtherComputers -and $OtherComputers.Count -gt 0) { | |
| Write-Host "`n=== Collecting Remote Firewall Logs ===" -ForegroundColor Cyan | |
| foreach ($computer in $OtherComputers) { | |
| try { | |
| $remotePath = "\\$computer\c$\Windows\System32\LogFiles\Firewall\$FWLog" | |
| Write-Host "Retrieving log from $computer..." -ForegroundColor Yellow | |
| if (Test-Path $remotePath) { | |
| $remoteLog = Get-ChildItem -Path $remotePath -ErrorAction Stop | |
| $remoteLogs += [PSCustomObject]@{ | |
| ComputerName = $computer | |
| Path = $remotePath | |
| LogObject = $remoteLog | |
| } | |
| Write-Host " SUCCESS: Retrieved log from $computer" -ForegroundColor Green | |
| } | |
| else { | |
| Write-Host " WARNING: Log not found at $remotePath" -ForegroundColor Yellow | |
| } | |
| } | |
| catch { | |
| Write-Host " ERROR: Failed to retrieve log from $computer - $($_.Exception.Message)" -ForegroundColor Red | |
| } | |
| } | |
| if ($remoteLogs.Count -eq 0) { | |
| Write-Host "WARNING: No remote logs could be retrieved" -ForegroundColor Yellow | |
| } | |
| } | |
| # Display comparison parameters | |
| Write-Host "`n=== Firewall Log Analysis ===" -ForegroundColor Cyan | |
| Write-Host "Local Log: $logPath" -ForegroundColor White | |
| if ($remoteLogs.Count -gt 0) { | |
| Write-Host "Remote Logs: $($remoteLogs.Count) server(s) - $($remoteLogs.ComputerName -join ', ')" -ForegroundColor White | |
| } | |
| $timeRange = if ($ForLastMins) { "$ForLastMins minutes" } | |
| elseif ($ForLastHours) { "$ForLastHours hours" } | |
| else { "All time" } | |
| Write-Host "Time Range: $timeRange" -ForegroundColor White | |
| Write-Host "Action Filter: $Action" -ForegroundColor White | |
| if ($Ports) { | |
| Write-Host "Port Filter: $($Ports -join ', ')" -ForegroundColor White | |
| } | |
| Write-Host "`nProcessing logs..." -ForegroundColor Yellow | |
| # Read and parse log file | |
| $logLines = Get-Content -Path $logPath | |
| $fieldsLine = $logLines | Where-Object { $_ -match '^#Fields:' } | Select-Object -First 1 | |
| if (-not $fieldsLine) { | |
| Write-Host "ERROR: No '#Fields:' line found in log file" -ForegroundColor Red | |
| return | |
| } | |
| $fields = $fieldsLine -replace '^#Fields:\s*', '' -split '\s+' | |
| $fieldIndex = @{} | |
| for ($i = 0; $i -lt $fields.Count; $i++) { | |
| $fieldIndex[$fields[$i]] = $i | |
| } | |
| # Compute time cutoff | |
| $cutoff = $null | |
| $hasTimeCutoff = $false | |
| if ($ForLastMins) { | |
| $cutoff = (Get-Date).AddMinutes(-[double]$ForLastMins) | |
| $hasTimeCutoff = $true | |
| } elseif ($ForLastHours) { | |
| $cutoff = (Get-Date).AddHours(-[double]$ForLastHours) | |
| $hasTimeCutoff = $true | |
| } | |
| $actionFilter = $null | |
| if ($Action -ne 'All') { | |
| $actionFilter = $Action.ToUpper() | |
| } | |
| $loopbackIPs = @('127.0.0.1','::1') | |
| # Helper function to check if IP is IPv6 | |
| function Test-IsIPv6 { | |
| param([string]$IP) | |
| return $IP -match ':' | |
| } | |
| # Parse outbound traffic (SEND) | |
| Write-Host "Parsing outbound traffic..." -ForegroundColor Yellow | |
| Write-Host "Note: Excluding localhost traffic (127.0.0.1, ::1)" -ForegroundColor White | |
| if (-not $CheckIPv6) { | |
| Write-Host "Note: Excluding IPv6 traffic (use -CheckIPv6 to include)" -ForegroundColor White | |
| } | |
| $outboundConnections = New-Object System.Collections.ArrayList | |
| $dataLines = $logLines | Where-Object { $_ -notmatch '^#' -and $_.Trim() -ne '' } | |
| # Pre-filter by time if cutoff specified for performance | |
| if ($hasTimeCutoff) { | |
| $cutoffStr = $cutoff.ToString('yyyy-MM-dd HH:mm:ss') | |
| $dataLines = $dataLines | Where-Object { | |
| $_ -ge $cutoffStr.Substring(0, 10) # Quick date string comparison | |
| } | |
| } | |
| [array]::Reverse($dataLines) | |
| foreach ($line in $dataLines) { | |
| $parts = $line -split '\s+' | |
| if ($parts.Count -lt $fields.Count) { | |
| continue | |
| } | |
| # Timestamp check | |
| $timestamp = $null | |
| if ($fieldIndex.ContainsKey('date') -and $fieldIndex.ContainsKey('time')) { | |
| $dateStr = $parts[$fieldIndex['date']] | |
| $timeStr = $parts[$fieldIndex['time']] | |
| try { | |
| $timestamp = Get-Date ("$dateStr $timeStr") -ErrorAction Stop | |
| } | |
| catch { | |
| $timestamp = $null | |
| } | |
| } | |
| if ($hasTimeCutoff -and $timestamp -and $timestamp -lt $cutoff) { | |
| break | |
| } | |
| # Action filter | |
| if ($actionFilter -and $fieldIndex.ContainsKey('action')) { | |
| $logAction = $parts[$fieldIndex['action']] | |
| if ($logAction -ne $actionFilter) { | |
| continue | |
| } | |
| } | |
| # Direction filter - only outbound (SEND) | |
| if ($fieldIndex.ContainsKey('direction')) { | |
| $dirVal = $parts[$fieldIndex['direction']] | |
| if ($dirVal -ne 'SEND') { | |
| continue | |
| } | |
| } | |
| # Extract connection details | |
| $srcIP = if ($fieldIndex.ContainsKey('src-ip')) { $parts[$fieldIndex['src-ip']] } else { $null } | |
| $dstIP = if ($fieldIndex.ContainsKey('dst-ip')) { $parts[$fieldIndex['dst-ip']] } else { $null } | |
| $srcPort = if ($fieldIndex.ContainsKey('src-port')) { $parts[$fieldIndex['src-port']] } else { $null } | |
| $dstPort = if ($fieldIndex.ContainsKey('dst-port')) { $parts[$fieldIndex['dst-port']] } else { $null } | |
| $protocol = if ($fieldIndex.ContainsKey('protocol')) { $parts[$fieldIndex['protocol']] } else { $null } | |
| $logAction = if ($fieldIndex.ContainsKey('action')) { $parts[$fieldIndex['action']] } else { $null } | |
| # Always exclude localhost traffic | |
| if ($loopbackIPs -contains $srcIP -or $loopbackIPs -contains $dstIP) { | |
| continue | |
| } | |
| # Port filter | |
| if ($Ports -and $Ports.Count -gt 0) { | |
| $matchPort = $false | |
| foreach ($p in $Ports) { | |
| $pStr = $p.ToString() | |
| if ($srcPort -eq $pStr -or $dstPort -eq $pStr) { | |
| $matchPort = $true | |
| break | |
| } | |
| } | |
| if (-not $matchPort) { | |
| continue | |
| } | |
| } | |
| # Create connection object | |
| $connection = [PSCustomObject]@{ | |
| Timestamp = $timestamp | |
| SourceIP = $srcIP | |
| SourcePort = $srcPort | |
| DestinationIP = $dstIP | |
| DestinationPort = $dstPort | |
| Protocol = $protocol | |
| Action = $logAction | |
| Matched = $false | |
| } | |
| [void]$outboundConnections.Add($connection) | |
| } | |
| Write-Host "Found $($outboundConnections.Count) outbound connections" -ForegroundColor Green | |
| # Parse inbound traffic (RECEIVE) from remote destination computers only | |
| Write-Host "Parsing inbound traffic on destination computers..." -ForegroundColor Yellow | |
| # Build hashtable of inbound connections for fast lookup | |
| $inboundConnections = @{} | |
| # Get local computer's IP addresses for matching | |
| $localIPs = New-Object 'System.Collections.Generic.HashSet[string]' | |
| try { | |
| [System.Net.Dns]::GetHostAddresses($env:COMPUTERNAME) | | |
| Where-Object { $_.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork } | | |
| ForEach-Object { [void]$localIPs.Add($_.ToString()) } | |
| } catch { | |
| Write-Host "WARNING: Could not resolve local IP addresses" -ForegroundColor Yellow | |
| } | |
| # Process remote logs for inbound traffic | |
| foreach ($remoteLog in $remoteLogs) { | |
| Write-Host "Processing inbound traffic from $($remoteLog.ComputerName)..." -ForegroundColor Yellow | |
| try { | |
| $remoteLines = Get-Content -Path $remoteLog.Path -ErrorAction Stop | |
| $remoteDataLines = $remoteLines | Where-Object { $_ -notmatch '^#' -and $_.Trim() -ne '' } | |
| # Pre-filter by time if cutoff specified for performance | |
| if ($hasTimeCutoff) { | |
| $cutoffStr = $cutoff.ToString('yyyy-MM-dd HH:mm:ss') | |
| $remoteDataLines = $remoteDataLines | Where-Object { | |
| $_ -ge $cutoffStr.Substring(0, 10) # Quick date string comparison | |
| } | |
| } | |
| [array]::Reverse($remoteDataLines) | |
| foreach ($line in $remoteDataLines) { | |
| $parts = $line -split '\s+' | |
| if ($parts.Count -lt $fields.Count) { | |
| continue | |
| } | |
| # Timestamp check | |
| $timestamp = $null | |
| if ($fieldIndex.ContainsKey('date') -and $fieldIndex.ContainsKey('time')) { | |
| $dateStr = $parts[$fieldIndex['date']] | |
| $timeStr = $parts[$fieldIndex['time']] | |
| try { | |
| $timestamp = Get-Date ("$dateStr $timeStr") -ErrorAction Stop | |
| } | |
| catch { | |
| $timestamp = $null | |
| } | |
| } | |
| if ($hasTimeCutoff -and $timestamp -and $timestamp -lt $cutoff) { | |
| break | |
| } | |
| # Action filter | |
| if ($actionFilter -and $fieldIndex.ContainsKey('action')) { | |
| $logAction = $parts[$fieldIndex['action']] | |
| if ($logAction -ne $actionFilter) { | |
| continue | |
| } | |
| } | |
| # Direction filter - only inbound (RECEIVE) | |
| if ($fieldIndex.ContainsKey('direction')) { | |
| $dirVal = $parts[$fieldIndex['direction']] | |
| if ($dirVal -ne 'RECEIVE') { | |
| continue | |
| } | |
| } | |
| # Extract connection details | |
| $srcIP = if ($fieldIndex.ContainsKey('src-ip')) { $parts[$fieldIndex['src-ip']] } else { $null } | |
| $dstIP = if ($fieldIndex.ContainsKey('dst-ip')) { $parts[$fieldIndex['dst-ip']] } else { $null } | |
| $dstPort = if ($fieldIndex.ContainsKey('dst-port')) { $parts[$fieldIndex['dst-port']] } else { $null } | |
| $protocol = if ($fieldIndex.ContainsKey('protocol')) { $parts[$fieldIndex['protocol']] } else { $null } | |
| # Only consider inbound traffic where source is from our local computer | |
| if ($localIPs.Count -gt 0 -and -not $localIPs.Contains($srcIP)) { | |
| continue | |
| } | |
| # Always exclude localhost traffic | |
| if ($loopbackIPs -contains $srcIP -or $loopbackIPs -contains $dstIP) { | |
| continue | |
| } | |
| # Exclude IPv6 unless CheckIPv6 specified | |
| if (-not $CheckIPv6 -and ((Test-IsIPv6 $srcIP) -or (Test-IsIPv6 $dstIP))) { | |
| continue | |
| } | |
| # Port filter | |
| if ($Ports -and $Ports.Count -gt 0) { | |
| $matchPort = $false | |
| foreach ($p in $Ports) { | |
| $pStr = $p.ToString() | |
| if ($dstPort -eq $pStr) { | |
| $matchPort = $true | |
| break | |
| } | |
| } | |
| if (-not $matchPort) { | |
| continue | |
| } | |
| } | |
| # Create lookup key | |
| $key = "$srcIP-$dstIP-$dstPort-$protocol" | |
| if (-not $inboundConnections.ContainsKey($key)) { | |
| $inboundConnections[$key] = New-Object System.Collections.ArrayList | |
| } | |
| [void]$inboundConnections[$key].Add($timestamp) | |
| } | |
| Write-Host " Processed $($remoteLog.ComputerName)" -ForegroundColor Green | |
| } | |
| catch { | |
| Write-Host " ERROR processing $($remoteLog.ComputerName): $($_.Exception.Message)" -ForegroundColor Red | |
| } | |
| } | |
| Write-Host "Found $($inboundConnections.Count) unique inbound connection patterns" -ForegroundColor Green | |
| # Compare and find matches | |
| Write-Host "`nComparing connections..." -ForegroundColor Yellow | |
| $matchedConnections = New-Object System.Collections.ArrayList | |
| $unmatchedConnections = New-Object System.Collections.ArrayList | |
| foreach ($outbound in $outboundConnections) { | |
| # Create lookup key for this outbound connection | |
| $key = "$($outbound.SourceIP)-$($outbound.DestinationIP)-$($outbound.DestinationPort)-$($outbound.Protocol)" | |
| if ($inboundConnections.ContainsKey($key)) { | |
| $outbound.Matched = $true | |
| [void]$matchedConnections.Add($outbound) | |
| } | |
| else { | |
| [void]$unmatchedConnections.Add($outbound) | |
| } | |
| } | |
| # Display results - focus on UNMATCHED (blocked/dropped) connections | |
| Write-Host "`n=== BLOCKED/UNRECEIVED TRAFFIC ===" -ForegroundColor Red | |
| Write-Host "Outbound traffic (SEND) that was NOT received on destination computer(s)" -ForegroundColor White | |
| Write-Host "This traffic may be blocked by firewall rules or dropped in transit`n" -ForegroundColor Yellow | |
| if ($unmatchedConnections.Count -gt 0) { | |
| $unmatchedConnections | | |
| Sort-Object Timestamp -Descending | | |
| Format-Table -AutoSize Timestamp, SourceIP, SourcePort, DestinationIP, DestinationPort, Protocol, Action | |
| } | |
| else { | |
| Write-Host "No blocked/unreceived connections found - all outbound traffic was received!" -ForegroundColor Green | |
| } | |
| # Display matched connections if requested | |
| if ($ShowUnmatched) { | |
| Write-Host "`n=== SUCCESSFUL CONNECTIONS ===" -ForegroundColor Green | |
| Write-Host "Outbound traffic (SEND) that was successfully received on destination computer(s)`n" -ForegroundColor White | |
| if ($unmatchedConnections.Count -gt 0) { | |
| $unmatchedConnections | | |
| Sort-Object Timestamp -Descending | | |
| Format-Table -AutoSize Timestamp, SourceIP, SourcePort, DestinationIP, DestinationPort, Protocol, Action | |
| } | |
| else { | |
| Write-Host "No unmatched connections found" -ForegroundColor Yellow | |
| } | |
| } | |
| # Summary | |
| Write-Host "`n=== SUMMARY ===" -ForegroundColor Cyan | |
| Write-Host "Total Outbound Connections Analyzed: $($outboundConnections.Count)" -ForegroundColor White | |
| Write-Host "Successfully Received: $($matchedConnections.Count)" -ForegroundColor Green | |
| Write-Host "Blocked/Unreceived: $($unmatchedConnections.Count)" -ForegroundColor $(if ($unmatchedConnections.Count -gt 0) { 'Red' } else { 'Green' }) | |
| if ($outboundConnections.Count -gt 0) { | |
| $successPercent = [math]::Round(($matchedConnections.Count / $outboundConnections.Count) * 100, 2) | |
| $blockedPercent = [math]::Round(($unmatchedConnections.Count / $outboundConnections.Count) * 100, 2) | |
| Write-Host "Success Rate: $successPercent%" -ForegroundColor $(if ($successPercent -ge 80) { 'Green' } elseif ($successPercent -ge 50) { 'Yellow' } else { 'Red' }) | |
| Write-Host "Blocked/Dropped Rate: $blockedPercent%" -ForegroundColor $(if ($blockedPercent -eq 0) { 'Green' } elseif ($blockedPercent -le 20) { 'Yellow' } else { 'Red' }) | |
| } | |
| Write-Host "" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment