Public/network/Get-ARPTable.ps1

#Requires -Version 5.1

function Get-ARPTable {
    <#
    .SYNOPSIS
        Retrieves the ARP (Address Resolution Protocol) cache as structured objects.
    .DESCRIPTION
        Parses the output of 'Get-NetNeighbor' cmdlet to return the local ARP cache
        as structured PowerShell objects. Each entry shows the IP address, MAC address,
        interface, and state of the ARP entry.
 
        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.
    .PARAMETER Credential
        Optional credential for remote computer connections.
    .PARAMETER State
        Filter by ARP entry state. Valid values: Reachable, Stale, Permanent, Unreachable, Incomplete.
    .PARAMETER AddressFamily
        Filter by address family: IPv4 or IPv6. Default: IPv4.
    .EXAMPLE
        Get-ARPTable
 
        Returns the local ARP cache (IPv4 entries).
    .EXAMPLE
        Get-ARPTable -State Reachable
 
        Returns only reachable ARP entries.
    .EXAMPLE
        Get-ARPTable -ComputerName 'SRV01' -Credential (Get-Credential)
 
        Returns the ARP cache from a remote server.
    .EXAMPLE
        'SRV01', 'SRV02' | Get-ARPTable
 
        Returns ARP tables from multiple servers via pipeline.
    .OUTPUTS
    PSWinOps.ArpEntry
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-03-21
        Requires: PowerShell 5.1+ / Windows only
        Requires: NetTCPIP module (built-in on Windows 8+/Server 2012+)
        Permissions: No admin required for reading ARP cache
    .LINK
    https://docs.microsoft.com/en-us/powershell/module/nettcpip/get-netneighbor
    #>

    [CmdletBinding()]
    [OutputType('PSWinOps.ArpEntry')]
    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('Reachable', 'Stale', 'Permanent', 'Unreachable', 'Incomplete')]
        [string]$State,

        [Parameter(Mandatory = $false)]
        [ValidateSet('IPv4', 'IPv6')]
        [string]$AddressFamily = 'IPv4'
    )

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

        $queryScriptBlock = {
            param([string]$QueryState, [string]$QueryAddressFamily)

            $getParams = @{
                ErrorAction   = 'Stop'
            }

            if ($QueryAddressFamily -eq 'IPv4') {
                $getParams['AddressFamily'] = 2
            } elseif ($QueryAddressFamily -eq 'IPv6') {
                $getParams['AddressFamily'] = 23
            }

            $entries = Get-NetNeighbor @getParams

            if ($QueryState) {
                $entries = @($entries | Where-Object { $_.State -eq $QueryState })
            }

            # Get interface aliases for enrichment
            $interfaces = @{}
            try {
                Get-NetAdapter -ErrorAction SilentlyContinue | ForEach-Object {
                    $interfaces[$_.ifIndex] = $_.Name
                }
            } catch {
                Write-Verbose "Failed to enumerate network adapters: $_"
            }

            foreach ($entry in $entries) {
                [PSCustomObject]@{
                    IPAddress      = $entry.IPAddress
                    LinkLayerAddr  = if ($entry.LinkLayerAddress) { $entry.LinkLayerAddress -replace '(..)', '$1:' -replace ':$' } else { '' }
                    State          = [string]$entry.State
                    InterfaceAlias = if ($interfaces.ContainsKey($entry.InterfaceIndex)) { $interfaces[$entry.InterfaceIndex] } else { "Index $($entry.InterfaceIndex)" }
                    InterfaceIndex = $entry.InterfaceIndex
                    AddressFamily  = if ($entry.AddressFamily -eq 2) { 'IPv4' } elseif ($entry.AddressFamily -eq 23) { 'IPv6' } else { [string]$entry.AddressFamily }
                }
            }
        }
    }

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

                Write-Verbose "[$($MyInvocation.MyCommand)] Querying ARP table on '$targetComputer'"

                $queryArgs = @(
                    $(if ($State) { $State } else { $null })
                    $AddressFamily
                )

                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.ArpEntry'
                        ComputerName   = $targetComputer
                        IPAddress      = $entry.IPAddress
                        MACAddress     = $entry.LinkLayerAddr
                        State          = $entry.State
                        InterfaceAlias = $entry.InterfaceAlias
                        InterfaceIndex = $entry.InterfaceIndex
                        AddressFamily  = $entry.AddressFamily
                        Timestamp      = $timestamp
                    }
                }
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_"
            }
        }
    }

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