Public/network/Get-ProcessByPort.ps1

#Requires -Version 5.1

function Get-ProcessByPort {
    <#
        .SYNOPSIS
            Correlate TCP/UDP endpoints with their owning process details
 
        .DESCRIPTION
            Joins listening and established TCP/UDP endpoints with the details of their
            owning process (name, executable path, command line) in a single command.
            Correlates each endpoint's OwningProcess with Win32_Process via CIM and works
            against local or remote computers.
 
            For remote computers, the query is executed via Invoke-Command.
 
        .PARAMETER ComputerName
            One or more computer names to query. Defaults to the local machine.
            Accepts pipeline input by value and by property name.
 
        .PARAMETER Credential
            Optional credential for remote computer connections.
 
        .PARAMETER Port
            Filter on LocalPort. Accepts multiple values. When omitted, all Listen
            and Established endpoints are returned.
 
        .PARAMETER State
            Filter TCP connections by state (Listen or Established). UDP endpoints
            are excluded from the results when State is specified, since UDP is
            stateless.
 
        .EXAMPLE
            Get-ProcessByPort
 
            Returns all TCP/UDP endpoints on the local machine with owning process details.
 
        .EXAMPLE
            Get-ProcessByPort -Port 443 -State Listen
 
            Shows which process is listening on TCP port 443 locally.
 
        .EXAMPLE
            Get-ProcessByPort -ComputerName 'SRV01' -Credential $cred
 
            Shows all endpoints and owning processes on a remote server.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Get-ProcessByPort -State Established
 
            Shows established TCP connections and owning processes on two remote servers.
 
        .OUTPUTS
            PSWinOps.ProcessPort
            One object per TCP/UDP endpoint, joined with owning process details.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-07-06
            Requires: PowerShell 5.1+ / Windows only
            Requires: Admin recommended for full process path/command line resolution
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.ProcessPort')]
    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)]
        [ValidateRange(1, 65535)]
        [int[]]$Port,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Listen', 'Established')]
        [string]$State
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting process-by-port query"

        $queryScriptBlock = {
            param([int[]]$FilterPorts, [string]$FilterState)

            # Build process cache (cast to [int] — OwningProcess is UInt32,
            # Win32_Process.ProcessId is UInt32; normalize before hashtable lookup)
            $processCache = @{}
            Get-CimInstance -ClassName Win32_Process -ErrorAction SilentlyContinue | ForEach-Object {
                $pidKey = [int]$_.ProcessId
                if (-not $processCache.ContainsKey($pidKey)) {
                    $processCache[$pidKey] = [PSCustomObject]@{
                        ProcessName = $_.Name
                        ProcessPath = $_.ExecutablePath
                        CommandLine = $_.CommandLine
                    }
                }
            }

            $results = [System.Collections.Generic.List[PSObject]]::new()

            # TCP connections (Listen + Established, optionally narrowed by -State)
            if (-not $FilterState -or $FilterState -in @('Listen', 'Established')) {
                $tcpParams = @{ ErrorAction = 'SilentlyContinue' }
                if ($FilterState) {
                    $tcpParams['State'] = $FilterState
                }

                $tcpConnections = Get-NetTCPConnection @tcpParams | Where-Object {
                    $_.State -in @('Listen', 'Established') -and (-not $FilterState -or $_.State.ToString() -eq $FilterState)
                }

                foreach ($conn in $tcpConnections) {
                    if ($FilterPorts -and $FilterPorts.Count -gt 0 -and $conn.LocalPort -notin $FilterPorts) {
                        continue
                    }

                    $ownerPid = [int]$conn.OwningProcess
                    $procInfo = $processCache[$ownerPid]

                    $results.Add([PSCustomObject]@{
                            Protocol      = 'TCP'
                            LocalAddress  = $conn.LocalAddress
                            LocalPort     = $conn.LocalPort
                            RemoteAddress = $conn.RemoteAddress
                            RemotePort    = $conn.RemotePort
                            State         = $conn.State.ToString()
                            ProcessId     = $ownerPid
                            ProcessName   = if ($procInfo) {
                                $procInfo.ProcessName
                            } else {
                                $null
                            }
                            ProcessPath   = if ($procInfo) {
                                $procInfo.ProcessPath
                            } else {
                                $null
                            }
                            CommandLine   = if ($procInfo) {
                                $procInfo.CommandLine
                            } else {
                                $null
                            }
                        })
                }
            }

            # UDP endpoints (stateless — excluded entirely when -State is specified)
            if (-not $FilterState) {
                $udpEndpoints = Get-NetUDPEndpoint -ErrorAction SilentlyContinue

                foreach ($ep in $udpEndpoints) {
                    if ($FilterPorts -and $FilterPorts.Count -gt 0 -and $ep.LocalPort -notin $FilterPorts) {
                        continue
                    }

                    $ownerPid = [int]$ep.OwningProcess
                    $procInfo = $processCache[$ownerPid]

                    $results.Add([PSCustomObject]@{
                            Protocol      = 'UDP'
                            LocalAddress  = $ep.LocalAddress
                            LocalPort     = $ep.LocalPort
                            RemoteAddress = $null
                            RemotePort    = $null
                            State         = $null
                            ProcessId     = $ownerPid
                            ProcessName   = if ($procInfo) {
                                $procInfo.ProcessName
                            } else {
                                $null
                            }
                            ProcessPath   = if ($procInfo) {
                                $procInfo.ProcessPath
                            } else {
                                $null
                            }
                            CommandLine   = if ($procInfo) {
                                $procInfo.CommandLine
                            } else {
                                $null
                            }
                        })
                }
            }

            return $results
        }
    }

    process {
        foreach ($targetComputer in $ComputerName) {
            try {
                $timestamp = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'

                Write-Verbose "[$($MyInvocation.MyCommand)] Querying process-by-port on '$targetComputer'"

                $queryArgs = @(
                    , $Port
                    $(if ($PSBoundParameters.ContainsKey('State')) {
                            $State
                        } else {
                            $null
                        })
                )

                $rawResults = Invoke-RemoteOrLocal -ComputerName $targetComputer -ScriptBlock $queryScriptBlock -ArgumentList $queryArgs -Credential $Credential

                foreach ($entry in $rawResults) {
                    [PSCustomObject]@{
                        PSTypeName    = 'PSWinOps.ProcessPort'
                        ComputerName  = $targetComputer
                        Protocol      = $entry.Protocol
                        LocalAddress  = $entry.LocalAddress
                        LocalPort     = $entry.LocalPort
                        RemoteAddress = $entry.RemoteAddress
                        RemotePort    = $entry.RemotePort
                        State         = $entry.State
                        ProcessId     = $entry.ProcessId
                        ProcessName   = $entry.ProcessName
                        ProcessPath   = $entry.ProcessPath
                        CommandLine   = $entry.CommandLine
                        Timestamp     = $timestamp
                    }
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_"
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed process-by-port query"
    }
}