GetNetTcpProcess.psm1
|
<#
.SYNOPSIS Lists TCP connections together with the process that owns each one. .DESCRIPTION Get-NetTcpProcess combines the output of Get-NetTCPConnection with process information from the Win32_Process CIM class, so you can see which process owns which TCP port. It works in Windows PowerShell 5.1 and PowerShell 7+ on Windows. Code by Joe Brown (joeb1kenobe). Feel free to use this code, but do not republish it as your own. If you discover a bug or see a way to improve the code, please submit a pull request at https://github.com/joeb1kenobe/GetNetTcpProcess .PARAMETER State Limits the output to connections in one or more TCP states: Closed, Listen, SynSent, SynReceived, Established, FinWait1, FinWait2, CloseWait, Closing, LastAck, TimeWait, DeleteTCB, Bound. .PARAMETER CommandLine Adds the ProcessCommandLine property: the command line used to start the owning process, if available. .PARAMETER Parent Adds the ParentProcessId and ParentProcess (parent process name) properties, if available. .EXAMPLE Get-NetTcpProcess -State Bound -CommandLine Lists all ports in the Bound state, with the command line used to start the owning process. .EXAMPLE Get-NetTcpProcess -State Listen, Established -Parent Lists all listening and established connections, with the parent of the owning process. .EXAMPLE Get-NetTcpProcess | Where-Object -Property Port -EQ 445 Shows the process that has local port 445 open. .OUTPUTS System.Management.Automation.PSCustomObject .NOTES Without elevation, Path, ProcessCommandLine and the parent of processes that run as another user (including SYSTEM services) may be empty. #> function Get-NetTcpProcess { [CmdletBinding()] [OutputType([pscustomobject])] param ( [ValidateSet('Closed', 'Listen', 'SynSent', 'SynReceived', 'Established', 'FinWait1', 'FinWait2', 'CloseWait', 'Closing', 'LastAck', 'TimeWait', 'DeleteTCB', 'Bound')] [string[]] $State, [switch] $CommandLine, [switch] $Parent ) # Let Get-NetTCPConnection filter by state. Filtering its unfiltered output # afterward is unreliable: with thousands of sockets (e.g. Docker Desktop), # the unfiltered query can report listening sockets as Closed. # # With -State, a state that has no connections produces an ObjectNotFound # error, which only means "no results". An advanced function's # -ErrorVariable collects errors from inner commands even when they are # caught or silenced, so run the query in a nested pipeline, whose errors # stay in its own Streams.Error. -ErrorAction Continue keeps a caller's # $ErrorActionPreference = 'Stop' from turning "no results" into a throw. $pipeline = [powershell]::Create([System.Management.Automation.RunspaceMode]::CurrentRunspace) try { $null = $pipeline.AddCommand('Get-NetTCPConnection').AddParameter('ErrorAction', 'Continue') if ($State) { $null = $pipeline.AddParameter('State', $State) } $connections = @($pipeline.Invoke()) foreach ($connectionError in $pipeline.Streams.Error) { if ($connectionError.CategoryInfo.Category -ne [System.Management.Automation.ErrorCategory]::ObjectNotFound) { $PSCmdlet.WriteError($connectionError) } } } catch [System.Management.Automation.MethodInvocationException] { # Surface the command's own terminating error, not the Invoke() wrapper. throw $_.Exception.InnerException } finally { $pipeline.Dispose() } if ($connections.Count -eq 0) { return } # Win32_Process exposes ParentProcessId and CommandLine in both Windows # PowerShell 5.1 and PowerShell 7+, unlike the Parent and CommandLine # properties of Get-Process, which exist only in PowerShell 7+. $cimProperties = @('ProcessId', 'Name', 'ExecutablePath') if ($Parent) { $cimProperties += 'ParentProcessId', 'CreationDate' } if ($CommandLine) { $cimProperties += 'CommandLine' } # Index processes by PID so each connection is an O(1) lookup. $processById = @{} foreach ($process in Get-CimInstance -ClassName Win32_Process -Property $cimProperties) { $processById[[int]$process.ProcessId] = $process } foreach ($connection in $connections) { $owningProcessId = [int]$connection.OwningProcess $process = $processById[$owningProcessId] $output = [ordered]@{ PSTypeName = 'NetTcpProcess' Address = [ipaddress]$connection.LocalAddress Port = [int]$connection.LocalPort ProcessName = $null ProcessId = $owningProcessId Path = $null } if ($process) { # Match the Get-Process naming used by earlier versions (no .exe). $output.ProcessName = $process.Name -replace '\.exe$' $output.Path = $process.ExecutablePath } if ($CommandLine) { $output.ProcessCommandLine = if ($process) { $process.CommandLine } else { $null } } if ($Parent) { $output.ParentProcessId = $null $output.ParentProcess = $null if ($process) { $parentProcessId = [int]$process.ParentProcessId $output.ParentProcessId = $parentProcessId # PID 0 is not a real parent: Windows reports it for System # Idle Process (whose "parent" is itself) and for System (4). $parentProcess = $null if ($parentProcessId -ne 0 -and $parentProcessId -ne $owningProcessId) { $parentProcess = $processById[$parentProcessId] } # PIDs are reused: a "parent" created after its child is an # unrelated process that inherited the exited parent's PID. $isRealParent = $parentProcess -and ( $null -eq $parentProcess.CreationDate -or $null -eq $process.CreationDate -or $parentProcess.CreationDate -le $process.CreationDate) if ($isRealParent) { $output.ParentProcess = $parentProcess.Name -replace '\.exe$' } } } # Some states (e.g. 0, seen on Docker Desktop's connections) have no # enum name, so State is $null. Show the raw CIM value instead. $output.State = [string]$connection.State if (-not $output.State -and $connection.CimInstanceProperties) { $output.State = [string]$connection.CimInstanceProperties['State'].Value } $output.RemoteAddress = [ipaddress]$connection.RemoteAddress $output.RemotePort = [int]$connection.RemotePort [pscustomobject]$output } } Export-ModuleMember -Function Get-NetTcpProcess |