Public/network/Show-NetworkStatisticMonitor.ps1
|
#Requires -Version 5.1 function Show-NetworkStatisticMonitor { <# .SYNOPSIS Interactive real-time monitor for TCP/UDP network connections .DESCRIPTION Renders a full-screen terminal UI showing active network connections with ANSI-colored protocol and state indicators, sortable columns, and interactive keyboard controls. Internally calls Get-NetworkConnection at each refresh interval and builds the display frame via StringBuilder for flicker-free rendering. Press Q to quit, S to cycle sort column, R to reverse sort order, or P to pause/resume data collection. .PARAMETER ComputerName One or more computer names to monitor. Accepts pipeline input by value and by property name. Defaults to the local machine ($env:COMPUTERNAME). .PARAMETER Credential Optional PSCredential for authenticating to remote machines via WinRM. Ignored for local machine queries. .PARAMETER Protocol Filter by protocol. Valid values: TCP, UDP. By default both are shown. .PARAMETER State Filter TCP connections by state (e.g. Established, Listen, TimeWait). Ignored for UDP endpoints (UDP is stateless). .PARAMETER LocalAddress Filter by local IP address. Supports wildcards. .PARAMETER LocalPort Filter by local port number. .PARAMETER RemoteAddress Filter by remote IP address. Supports wildcards. .PARAMETER RemotePort Filter by remote port number. .PARAMETER ProcessName Filter by owning process name. Supports wildcards. .PARAMETER RefreshInterval Refresh interval in seconds. Default: 2. Valid range: 1-300 seconds. .PARAMETER NoClear Suppresses the console clear on exit so the final frame remains visible in the scrollback buffer. .PARAMETER NoColor Disables ANSI color output for terminals that do not support escape sequences. .EXAMPLE Show-NetworkStatisticMonitor Starts real-time monitoring of all network connections on the local machine. Press Q to quit, S to cycle sort, R to reverse, P to pause. .EXAMPLE Show-NetworkStatisticMonitor -Protocol TCP -State Established -RefreshInterval 5 Monitors only established TCP connections with a 5-second refresh interval. .EXAMPLE 'SRV01', 'SRV02' | Show-NetworkStatisticMonitor -Protocol TCP -NoColor Monitors TCP connections on two remote servers without ANSI colors. .OUTPUTS None This function renders an interactive TUI and does not produce pipeline output. .NOTES Author: Franck SALLET Version: 2.0.0 Last Modified: 2026-04-11 Requires: PowerShell 5.1+ / Windows only Requires: Interactive console (not ISE or redirected output) .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/powershell/module/nettcpip/get-nettcpconnection #> [CmdletBinding()] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseDeclaredVarsMoreThanAssignments', '')] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '')] [OutputType([void])] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [PSCredential]$Credential, [Parameter(Mandatory = $false)] [ValidateSet('TCP', 'UDP')] [string[]]$Protocol = @('TCP', 'UDP'), [Parameter(Mandatory = $false)] [ValidateSet('Bound', 'Closed', 'CloseWait', 'Closing', 'DeleteTCB', 'Established', 'FinWait1', 'FinWait2', 'LastAck', 'Listen', 'SynReceived', 'SynSent', 'TimeWait')] [string[]]$State, [Parameter(Mandatory = $false)] [SupportsWildcards()] [string]$LocalAddress, [Parameter(Mandatory = $false)] [ValidateRange(1, 65535)] [int]$LocalPort, [Parameter(Mandatory = $false)] [SupportsWildcards()] [string]$RemoteAddress, [Parameter(Mandatory = $false)] [ValidateRange(1, 65535)] [int]$RemotePort, [Parameter(Mandatory = $false)] [SupportsWildcards()] [string]$ProcessName, [Parameter(Mandatory = $false)] [ValidateRange(1, 300)] [int]$RefreshInterval = 2, [Parameter(Mandatory = $false)] [switch]$NoClear, [Parameter(Mandatory = $false)] [switch]$NoColor ) begin { if ($Host.Name -eq 'Windows PowerShell ISE Host') { Write-Error -Message "[$($MyInvocation.MyCommand)] ISE is not supported. Use Windows Terminal, ConHost, or a remote SSH session." return } Write-Verbose "[$($MyInvocation.MyCommand)] Starting network statistics monitor" $allComputers = [System.Collections.Generic.List[string]]::new() $getStatParams = @{} if ($PSBoundParameters.ContainsKey('Credential')) { $getStatParams['Credential'] = $Credential } if ($PSBoundParameters.ContainsKey('Protocol')) { $getStatParams['Protocol'] = $Protocol } if ($PSBoundParameters.ContainsKey('State')) { $getStatParams['State'] = $State } if ($PSBoundParameters.ContainsKey('LocalAddress')) { $getStatParams['LocalAddress'] = $LocalAddress } if ($PSBoundParameters.ContainsKey('LocalPort')) { $getStatParams['LocalPort'] = $LocalPort } if ($PSBoundParameters.ContainsKey('RemoteAddress')) { $getStatParams['RemoteAddress'] = $RemoteAddress } if ($PSBoundParameters.ContainsKey('RemotePort')) { $getStatParams['RemotePort'] = $RemotePort } if ($PSBoundParameters.ContainsKey('ProcessName')) { $getStatParams['ProcessName'] = $ProcessName } # ---- ANSI helpers ---- $esc = [char]27 $useColor = -not $NoColor $dim = if ($useColor) { "${esc}[90m" } else { '' } $reset = if ($useColor) { "${esc}[0m" } else { '' } $bold = if ($useColor) { "${esc}[1m" } else { '' } $cyan = if ($useColor) { "${esc}[96m" } else { '' } $white = if ($useColor) { "${esc}[97m" } else { '' } $yellow = if ($useColor) { "${esc}[93m" } else { '' } $green = if ($useColor) { "${esc}[92m" } else { '' } $red = if ($useColor) { "${esc}[91m" } else { '' } $underline = if ($useColor) { "${esc}[4m" } else { '' } function Get-VisualWidth { param([string]$Text) ($Text -replace "$([char]27)\[\d+(?:;\d+)*m", '').Length } function ConvertTo-PaddedLine { param([string]$Text, [int]$TargetWidth) $visual = Get-VisualWidth -Text $Text $needed = $TargetWidth - $visual if ($needed -gt 0) { return $Text + [string]::new(' ', $needed) } return $Text } function Get-ProtocolColor { param([string]$Proto) if (-not $script:useColor) { return '' } if ($Proto -eq 'TCP') { return $script:cyan } if ($Proto -eq 'UDP') { return $script:yellow } return '' } function Get-StateColor { param([string]$ConnState) if (-not $script:useColor) { return '' } switch ($ConnState) { 'Established' { return $script:green } 'Listen' { return "$($script:white)$($script:bold)" } 'TimeWait' { return $script:dim } 'CloseWait' { return $script:red } 'Closing' { return $script:red } 'LastAck' { return $script:red } default { return '' } } } $sortModes = @('Process', 'Protocol', 'State', 'LocalPort', 'RemoteAddr') $sortModeIndex = 0 $sortDescending = $false $paused = $false $running = $true $lastResults = @() } process { if ($Host.Name -eq 'Windows PowerShell ISE Host') { return } foreach ($computer in $ComputerName) { $allComputers.Add($computer) } } end { if ($Host.Name -eq 'Windows PowerShell ISE Host') { return } $previousCtrlC = [Console]::TreatControlCAsInput $previousCursorVisible = [Console]::CursorVisible try { [Console]::TreatControlCAsInput = $true [Console]::CursorVisible = $false [Console]::Clear() while ($running) { $frameStart = [Diagnostics.Stopwatch]::StartNew() $currentSortMode = $sortModes[$sortModeIndex] # ---- Data gathering (skip when paused) ---- if (-not $paused) { $lastResults = @(Get-NetworkConnection -ComputerName $allComputers.ToArray() @getStatParams -ErrorAction SilentlyContinue) } $connectionCount = $lastResults.Count $width = [math]::Max(80, [Console]::WindowWidth) $height = [math]::Max(24, [Console]::WindowHeight) $computerList = $allComputers -join ', ' # ---- Sort data ---- $sortProperty = switch ($currentSortMode) { 'Process' { 'ProcessName' } 'Protocol' { 'Protocol' } 'State' { 'State' } 'LocalPort' { 'LocalPort' } 'RemoteAddr' { 'RemoteAddress' } } $sortedResults = if ($connectionCount -gt 0) { $lastResults | Sort-Object -Property $sortProperty -Descending:$sortDescending } else { @() } # ---- Build frame ---- $frame = [System.Text.StringBuilder]::new(4096) $lineCount = 0 $separator = [string]::new('-', [math]::Min($width - 4, 120)) # Header $timeStr = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' $pauseIndicator = if ($paused) { " ${red}${bold}(PAUSED)${reset}" } else { '' } $headerLine = " ${bold}${cyan}Network Monitor${reset} ${dim}-${reset} ${bold}${white}${computerList}${reset}${pauseIndicator} ${dim}|${reset} ${dim}${timeStr}${reset}" [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $headerLine -TargetWidth $width)) $lineCount++ [void]$frame.AppendLine((ConvertTo-PaddedLine -Text " ${dim}${separator}${reset}" -TargetWidth $width)) $lineCount++ # Column widths $colProto = 7; $colLAddr = 23; $colLPort = 12 $colRAddr = 23; $colRPort = 13; $colState = 14; $colProc = 20 # Sort direction arrow $sortArrow = if ($sortDescending) { if ($useColor) { "${cyan}v${reset}" } else { 'v' } } else { if ($useColor) { "${cyan}^${reset}" } else { '^' } } # Table header with active sort highlighted $protoH = if ($currentSortMode -eq 'Protocol') { "${cyan}${underline}PROTO${reset}" } else { "${dim}PROTO${reset}" } $lAddrH = "${dim}LOCAL ADDRESS${reset}" $lPortH = if ($currentSortMode -eq 'LocalPort') { "${cyan}${underline}LOCAL PORT${reset}" } else { "${dim}LOCAL PORT${reset}" } $rAddrH = if ($currentSortMode -eq 'RemoteAddr') { "${cyan}${underline}REMOTE ADDRESS${reset}" } else { "${dim}REMOTE ADDRESS${reset}" } $rPortH = "${dim}REMOTE PORT${reset}" $stateH = if ($currentSortMode -eq 'State') { "${cyan}${underline}STATE${reset}" } else { "${dim}STATE${reset}" } $procH = if ($currentSortMode -eq 'Process') { "${cyan}${underline}PROCESS${reset}" } else { "${dim}PROCESS${reset}" } $headerRow = ' {0}{1}{2}{3}{4}{5}{6}' -f ` (ConvertTo-PaddedLine -Text $protoH -TargetWidth $colProto), (ConvertTo-PaddedLine -Text $lAddrH -TargetWidth $colLAddr), (ConvertTo-PaddedLine -Text $lPortH -TargetWidth $colLPort), (ConvertTo-PaddedLine -Text $rAddrH -TargetWidth $colRAddr), (ConvertTo-PaddedLine -Text $rPortH -TargetWidth $colRPort), (ConvertTo-PaddedLine -Text $stateH -TargetWidth $colState), $procH [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $headerRow -TargetWidth $width)) $lineCount++ $dashRow = " ${dim}{0}{1}{2}{3}{4}{5}{6}${reset}" -f ` ('{0,-7}' -f '-----'), ('{0,-23}' -f '-------------'), ('{0,-12}' -f '----------'), ('{0,-23}' -f '--------------'), ('{0,-13}' -f '-----------'), ('{0,-14}' -f '-----'), '-------' [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $dashRow -TargetWidth $width)) $lineCount++ # Data rows $availableRows = $height - $lineCount - 4 if ($connectionCount -gt 0) { $displayCount = [math]::Min($sortedResults.Count, [math]::Max(5, $availableRows)) for ($i = 0; $i -lt $displayCount; $i++) { $conn = $sortedResults[$i] $protoColor = Get-ProtocolColor -Proto $conn.Protocol $stateColor = Get-StateColor -ConnState $conn.State $dataRow = ' {0} {1} {2} {3} {4} {5} {6}' -f ` "${protoColor}$('{0,-5}' -f $conn.Protocol)${reset}", ('{0,-21}' -f $conn.LocalAddress), ('{0,-10}' -f $conn.LocalPort), ('{0,-21}' -f $conn.RemoteAddress), ('{0,-11}' -f $conn.RemotePort), "${stateColor}$('{0,-12}' -f $conn.State)${reset}", "${white}$($conn.ProcessName)${reset}" [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $dataRow -TargetWidth $width)) $lineCount++ } } else { [void]$frame.AppendLine((ConvertTo-PaddedLine -Text " ${yellow}(No matching connections found)${reset}" -TargetWidth $width)) $lineCount++ } # Footer [void]$frame.AppendLine('') $lineCount++ [void]$frame.AppendLine((ConvertTo-PaddedLine -Text " ${dim}${separator}${reset}" -TargetWidth $width)) $lineCount++ $footerLine = " ${bold}[${cyan}Q${reset}${bold}]${reset}uit ${bold}[${cyan}S${reset}${bold}]${reset}ort ${bold}[${cyan}R${reset}${bold}]${reset}everse ${bold}[${cyan}P${reset}${bold}]${reset}ause ${dim}|${reset} Refresh: ${yellow}${RefreshInterval}s${reset} ${dim}|${reset} Sort: ${cyan}${bold}${currentSortMode}${reset} ${sortArrow} ${dim}|${reset} Connections: ${white}${connectionCount}${reset}" [void]$frame.AppendLine((ConvertTo-PaddedLine -Text $footerLine -TargetWidth $width)) $lineCount++ # Erase trailing lines $remainingRows = $height - $lineCount for ($r = 0; $r -lt $remainingRows; $r++) { [void]$frame.AppendLine("${esc}[2K") } # Single write [Console]::SetCursorPosition(0, 0) [Console]::Write($frame.ToString()) $frameStart.Stop() # ---- Input handling ---- $sleepMs = [math]::Max(100, ($RefreshInterval * 1000) - $frameStart.ElapsedMilliseconds) $inputTimer = [Diagnostics.Stopwatch]::StartNew() while ($inputTimer.ElapsedMilliseconds -lt $sleepMs) { if ([Console]::KeyAvailable) { $key = [Console]::ReadKey($true) # Ctrl+C always wins if ($key.Key -eq 'C' -and ($key.Modifiers -band [ConsoleModifiers]::Control)) { $running = $false break } if ($key.Key -eq 'Q' -or $key.Key -eq 'Escape') { $running = $false } elseif ($key.Key -eq 'S') { $sortModeIndex = ($sortModeIndex + 1) % $sortModes.Count } elseif ($key.Key -eq 'P') { $paused = -not $paused } elseif ($key.Key -eq 'R') { $sortDescending = -not $sortDescending } if (-not $running) { break } } Start-Sleep -Milliseconds 50 } } } finally { [Console]::CursorVisible = $previousCursorVisible [Console]::TreatControlCAsInput = $previousCtrlC if (-not $NoClear) { [Console]::Clear() } Write-Information -MessageData 'Network Statistics Monitor stopped.' -InformationAction Continue } } } |