Public/network/Get-NetworkConnection.ps1

#Requires -Version 5.1

function Get-NetworkConnection {
    <#
        .SYNOPSIS
            Retrieves TCP and UDP network connections enriched with process info
 
        .DESCRIPTION
            Returns one object per TCP or UDP connection on the target machine, including
            local and remote address/port, connection state, and the owning process name.
            Supports both local and remote queries via WinRM. Filtering is available on
            protocol, state, address, port, and process name.
 
            For the local machine, cmdlets are called directly. For remote machines,
            the query is executed via Invoke-Command with WinRM.
 
        .PARAMETER ComputerName
            One or more computer names to query. 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. Ignored for
            local machine queries.
 
        .PARAMETER Protocol
            Filter by protocol. Valid values: TCP, UDP. By default both are returned.
 
        .PARAMETER State
            Filter TCP connections by state (e.g. Established, Listen, TimeWait, CloseWait).
            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.
 
        .EXAMPLE
            Get-NetworkStatistic
 
            Returns all TCP and UDP connections on the local machine.
 
        .EXAMPLE
            Get-NetworkStatistic -Protocol TCP -State Established
 
            Returns only established TCP connections on the local machine.
 
        .EXAMPLE
            Get-NetworkStatistic -ComputerName 'SRV01', 'SRV02' -Protocol TCP -State Listen
 
            Returns listening TCP connections on two remote servers.
 
        .EXAMPLE
            Get-NetworkStatistic -ProcessName 'svchost' -LocalPort 443
 
            Returns connections on local port 443 owned by svchost.
 
        .EXAMPLE
            'SRV01', 'SRV02' | Get-NetworkStatistic -Credential (Get-Credential) -Protocol TCP
 
            Queries multiple servers via pipeline with explicit credentials.
 
        .OUTPUTS
            PSWinOps.NetworkConnection
            One object per connection with ComputerName, Protocol, LocalAddress,
            LocalPort, RemoteAddress, RemotePort, State, ProcessId, ProcessName,
            and Timestamp properties.
 
        .NOTES
            Author: Franck SALLET
            Version: 1.0.0
            Last Modified: 2026-03-23
            Requires: PowerShell 5.1+ / Windows only
            Permissions: No admin required for basic queries
            Remote: Requires WinRM / WS-Man enabled on target machines
 
        .LINK
            https://github.com/k9fr4n/PSWinOps
 
        .LINK
            https://learn.microsoft.com/en-us/powershell/module/nettcpip/get-nettcpconnection
    #>


    [CmdletBinding()]
    [OutputType('PSWinOps.NetworkConnection')]
    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
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting network connection query"
        $localNames = @($env:COMPUTERNAME, 'localhost', '.')
        $hasCredential = $PSBoundParameters.ContainsKey('Credential')

        # Build the scriptblock that collects connections (runs locally or via Invoke-Command)
        $queryScriptBlock = {
            param(
                [string[]]$QueryProtocol,
                [string[]]$QueryState,
                [string]$QueryLocalAddress,
                [int]$QueryLocalPort,
                [string]$QueryRemoteAddress,
                [int]$QueryRemotePort,
                [string]$QueryProcessName
            )

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

            # Build process lookup table once (cast to [int] — OwningProcess is UInt32,
            # Process.Id is Int32; mismatched key types cause lookup failures)
            $processLookup = @{}
            foreach ($proc in (Get-Process -ErrorAction SilentlyContinue)) {
                $pidKey = [int]$proc.Id
                if (-not $processLookup.ContainsKey($pidKey)) {
                    $processLookup[$pidKey] = $proc.ProcessName
                }
            }

            # TCP connections
            if ($QueryProtocol -contains 'TCP') {
                $tcpParams = @{ ErrorAction = 'SilentlyContinue' }
                if ($QueryState) {
                    $tcpParams['State'] = $QueryState
                }
                $tcpConnections = Get-NetTCPConnection @tcpParams

                foreach ($conn in $tcpConnections) {
                    $ownerPid = [int]$conn.OwningProcess
                    $procName = if ($processLookup.ContainsKey($ownerPid)) {
                        $processLookup[$ownerPid]
                    } else {
                        'Unknown'
                    }

                    $obj = [PSCustomObject]@{
                        Protocol      = 'TCP'
                        LocalAddress  = $conn.LocalAddress
                        LocalPort     = $conn.LocalPort
                        RemoteAddress = $conn.RemoteAddress
                        RemotePort    = $conn.RemotePort
                        State         = [string]$conn.State
                        ProcessId     = $conn.OwningProcess
                        ProcessName   = $procName
                    }
                    $results.Add($obj)
                }
            }

            # UDP endpoints
            if ($QueryProtocol -contains 'UDP') {
                $udpEndpoints = Get-NetUDPEndpoint -ErrorAction SilentlyContinue

                foreach ($ep in $udpEndpoints) {
                    $ownerPid = [int]$ep.OwningProcess
                    $procName = if ($processLookup.ContainsKey($ownerPid)) {
                        $processLookup[$ownerPid]
                    } else {
                        'Unknown'
                    }

                    $obj = [PSCustomObject]@{
                        Protocol      = 'UDP'
                        LocalAddress  = $ep.LocalAddress
                        LocalPort     = $ep.LocalPort
                        RemoteAddress = '*'
                        RemotePort    = 0
                        State         = 'Stateless'
                        ProcessId     = $ep.OwningProcess
                        ProcessName   = $procName
                    }
                    $results.Add($obj)
                }
            }

            # Apply client-side filters
            $filtered = $results

            if ($QueryLocalAddress) {
                $filtered = @($filtered | Where-Object { $_.LocalAddress -like $QueryLocalAddress })
            }
            if ($QueryLocalPort -gt 0) {
                $filtered = @($filtered | Where-Object { $_.LocalPort -eq $QueryLocalPort })
            }
            if ($QueryRemoteAddress) {
                $filtered = @($filtered | Where-Object { $_.RemoteAddress -like $QueryRemoteAddress })
            }
            if ($QueryRemotePort -gt 0) {
                $filtered = @($filtered | Where-Object { $_.RemotePort -eq $QueryRemotePort })
            }
            if ($QueryProcessName) {
                $filtered = @($filtered | Where-Object { $_.ProcessName -like $QueryProcessName })
            }

            $filtered
        }
    }

    process {

        foreach ($targetComputer in $ComputerName) {
            try {
                $isLocal = $localNames -contains $targetComputer
                $timestamp = Get-Date -Format 'o'

                Write-Verbose "[$($MyInvocation.MyCommand)] Querying '$targetComputer' (local: $isLocal)"

                $queryArgs = @(
                    , $Protocol
                    $(if ($State) {
                            , $State
                        } else {
                            , $null
                        })
                    $(if ($PSBoundParameters.ContainsKey('LocalAddress')) {
                            $LocalAddress
                        } else {
                            $null
                        })
                    $(if ($PSBoundParameters.ContainsKey('LocalPort')) {
                            $LocalPort
                        } else {
                            0
                        })
                    $(if ($PSBoundParameters.ContainsKey('RemoteAddress')) {
                            $RemoteAddress
                        } else {
                            $null
                        })
                    $(if ($PSBoundParameters.ContainsKey('RemotePort')) {
                            $RemotePort
                        } else {
                            0
                        })
                    $(if ($PSBoundParameters.ContainsKey('ProcessName')) {
                            $ProcessName
                        } else {
                            $null
                        })
                )

                if ($isLocal) {
                    $rawResults = & $queryScriptBlock @queryArgs
                } else {
                    $invokeParams = @{
                        ComputerName = $targetComputer
                        ScriptBlock  = $queryScriptBlock
                        ArgumentList = $queryArgs
                        ErrorAction  = 'Stop'
                    }
                    if ($hasCredential) {
                        $invokeParams['Credential'] = $Credential
                    }
                    $rawResults = Invoke-Command @invokeParams
                }

                foreach ($entry in $rawResults) {
                    [PSCustomObject]@{
                        PSTypeName    = 'PSWinOps.NetworkConnection'
                        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
                        Timestamp     = $timestamp
                    }
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_"
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed network connection query"
    }
}