PowerShell Genie

Map active TCP connections to processes

Script 3 min read Windows PowerShell 5.1 and PowerShell 7

Documentation: Microsoft Learn

Established connections come back from Get-NetTCPConnection, and ForEach-Object builds a fresh [pscustomobject] per row carrying the local port with the owning process name.

The script

1PS> Get-NetTCPConnection -State Established | ForEach-Object { [pscustomobject]@{LocalPort= $_.LocalPort;RemoteAddress= $_.RemoteAddress;RemotePort= $_.RemotePort;Process=(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName} }

Script reader

Inspect before you run

Inspection only

Inspect the script below. It does not run in this browser.

Inspection only — no command has run.

    Show reference output
    1 LocalAddress LocalPort RemoteAddress RemotePort State OwningProcess
    2 ------------ --------- -------------- ---------- ----------- -------------
    3 10.20.30.41 51544 13.107.42.14 443 Established 9184
    4 10.20.30.41 51612 20.190.151.132 443 Established 6620
    Inspect this example’s structure

    Read the command and its supplied arguments. This example requires real PowerShell or its module; this diagram does not execute it.

    Get-NetTCPConnection -State Established | ForEach-Object { [pscustomobject]@{LocalPort= $_.LocalPort;RemoteAddress= $_.RemoteAddress;RemotePort= $_.RemotePort;Process=(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName} }
    1. Stage 1Get-NetTCPConnection-State
    2. Stage 2ForEach-ObjectPositional values or no named parameters

      How it works

      Established connections come back from Get-NetTCPConnection, and ForEach-Object builds a fresh [pscustomobject] per row carrying the local port with the owning process name. The Process field calls Get-Process against OwningProcess inside the loop and reads ProcessName from the result, with -EA SilentlyContinue absorbing the lookup when the process has already exited.

      Adapting it

      Reading Path instead of ProcessName gives the full executable location, which matters when two unrelated binaries share a name. Grouping the finished output on Process turns a socket list into a per-application summary.

      If the result is unexpected

      The Process column is blank on several rows, and the command takes an unreasonable amount of time to finish on a busy server.

      Each row triggers its own process lookup, and a short-lived connection whose process has already exited returns nothing, which the suppressed error turns into an empty field. On a host holding thousands of connections that is thousands of separate lookups, and the elapsed time reflects exactly that. An unelevated session also cannot open processes owned by other accounts.

      1. Build the process list once before the loop and index it into a hashtable keyed by Id.
      2. Rerun elevated and check whether previously blank names are now populated.
      3. Compare the connection count against the total from Get-NetTCPConnection to gauge how much work the loop is doing.
      4. Drop connections in TimeWait first, since those frequently have no live owning process at all.
      5. Keep the PID in the output alongside the name so a failed lookup still leaves something to investigate.

      Validate the result

      Every row carries either a process name or a PID that can be accounted for, and the command finishes in seconds rather than minutes.

      Keep the check reliable

      Cache lookups outside loops whenever the same query would otherwise repeat per row, which is the general fix for this whole class of slowness.

      Primary documentation

      ForEach-Object reference · Get-Process reference

      Continue
      Genie guideMap active TCP connections to processes

      Established connections come back from Get-NetTCPConnection, and ForEach-Object builds a fresh [pscustomobject] per row carrying the local port with the owning process name.