PowerShell TCP Port Scanner: Fast Multi-Host Port Checks

PowerShell TCP Port Scanner: Fast Multi-Host Port Checks

PowerShell Tips Editor 4 min read
PowerShell TCP Port Scanner: Fast Multi-Host Port Checks

Before pushing a firewall change to production, you need to know which ports are actually reachable across your server estate. Dedicated tools like nmap are powerful, but they require installation, elevated privileges, and often trigger security alerts. A PowerShell port scanner uses built-in cmdlets, runs under normal admin credentials, and can output directly to the CSV or HTML formats your change management process requires — no third-party software needed.

Quick Answer

Use Test-NetConnection -ComputerName host -Port 443 -InformationLevel Quiet for quick single-port checks, then wrap it in ForEach-Object -Parallel (PowerShell 7+) to scan multiple hosts and ports simultaneously and collect results into a structured report.

Using Test-NetConnection -Port for Single Port Checks

Test-NetConnection is the cmdlet-native way to probe a TCP port. Without -InformationLevel Quiet it prints status messages to the information stream — useful interactively, slow in bulk.

# Basic check — returns rich object
$result = Test-NetConnection -ComputerName "dc01.corp.local" -Port 389

$result.TcpTestSucceeded   # True / False
$result.PingSucceeded
$result.RemoteAddress

# Quiet mode — returns only $true or $false, much faster for closed ports
$isOpen = Test-NetConnection -ComputerName "web01" -Port 443 -InformationLevel Quiet
Write-Host "Port 443 open: $isOpen"

For closed ports, Test-NetConnection waits for a TCP timeout before returning $false. Using -InformationLevel Quiet suppresses the warning output but does not shorten the timeout. When scanning many hosts, parallelism is essential.

Building a Port List and Host List from CSV

Define your scan targets in a pair of CSV files so the scanner is reusable without script edits. A hosts CSV and a ports CSV keep concerns separated.

# hosts.csv
# ComputerName
# web01.corp.local
# web02.corp.local
# sql01.corp.local

# ports.csv
# Port,Description
# 80,HTTP
# 443,HTTPS
# 1433,SQL Server
# 3389,RDP

$hosts = Import-Csv ".\hosts.csv"
$ports = Import-Csv ".\ports.csv"

Write-Host "Scanning $($hosts.Count) hosts across $($ports.Count) ports..."

Parallelising with ForEach-Object -Parallel for Speed

ForEach-Object -Parallel requires PowerShell 7+. It runs each iteration in a separate runspace, dramatically reducing total scan time. Use $using: to pass variables from the caller scope into the parallel block.

#Requires -Version 7

$scanResults = $hosts | ForEach-Object -Parallel {
    $hostName  = $_.ComputerName
    $portList  = $using:ports

    foreach ($p in $portList) {
        $portNum = [int]$p.Port
        $open    = (Test-NetConnection -ComputerName $hostName `
                                       -Port $portNum `
                                       -InformationLevel Quiet `
                                       -ErrorAction SilentlyContinue) -eq $true
        [PSCustomObject]@{
            Host        = $hostName
            Port        = $portNum
            Description = $p.Description
            Open        = $open
            Timestamp   = (Get-Date -Format "o")
        }
    }
} -ThrottleLimit 20

Set -ThrottleLimit to a value that keeps your network from being overwhelmed. Twenty concurrent runspaces is a practical starting point; tune upward on fast LANs.

Capturing TcpTestSucceeded and Latency in Results

For a richer dataset, drop the Quiet flag and capture the full Test-NetConnection object, including ping round-trip time alongside TCP result.

$detailed = $hosts | ForEach-Object -Parallel {
    $hostName = $_.ComputerName
    $portList = $using:ports

    foreach ($p in $portList) {
        $portNum = [int]$p.Port
        $result  = Test-NetConnection -ComputerName $hostName `
                                      -Port $portNum `
                                      -ErrorAction SilentlyContinue
        [PSCustomObject]@{
            Host          = $hostName
            Port          = $portNum
            Description   = $p.Description
            TcpOpen       = $result.TcpTestSucceeded
            PingRTT_ms    = $result.PingReplyDetails.RoundtripTime
            RemoteAddress = $result.RemoteAddress
        }
    }
} -ThrottleLimit 15

Generating a Host x Port Connectivity Matrix

A matrix layout — hosts as rows, ports as columns — is the most readable summary for firewall review meetings.

$portNums = $ports.Port | Sort-Object

$matrix = $scanResults | Group-Object Host | ForEach-Object {
    $row  = [ordered]@{ Host = $_.Name }
    $data = $_.Group
    foreach ($port in $portNums) {
        $entry = $data | Where-Object { $_.Port -eq [int]$port }
        $row["Port_$port"] = if ($entry) { if ($entry.Open) { "OPEN" } else { "CLOSED" } } else { "N/A" }
    }
    [PSCustomObject]$row
}

$matrix | Format-Table -AutoSize

Exporting Results to CSV and HTML

Export both formats: CSV for import into ticketing systems, HTML for email or wiki attachment.

$datestamp = Get-Date -Format "yyyyMMdd-HHmm"

# CSV export
$scanResults | Export-Csv "PortScan-$datestamp.csv" -NoTypeInformation

# HTML export with basic styling
$htmlBody = $scanResults |
    ConvertTo-Html -Property Host, Port, Description, Open, Timestamp `
                   -PreContent "<h2>Port Scan Results — $datestamp</h2>" `
                   -PostContent "<p>Generated by PowerShell port scanner</p>"

$htmlBody | Set-Content "PortScan-$datestamp.html" -Encoding UTF8
Write-Host "Reports saved: PortScan-$datestamp.csv and .html"

Common Errors

  • Test-NetConnection is slow for closed ports: Without -InformationLevel Quiet, closed-port checks wait for full TCP timeout and emit warning messages to the screen, making bulk scans appear to hang. Always use -InformationLevel Quiet in automated scans and suppress errors with -ErrorAction SilentlyContinue.
  • Parallel result objects not thread-safe when writing to a shared list: Do not append to a regular [System.Collections.Generic.List[object]] from inside ForEach-Object -Parallel — concurrent writes cause data loss or exceptions. Instead, let each parallel block return objects via the pipeline as shown above, and PowerShell will collect them safely.

Related Cmdlets / See Also

Wrapping Up

A PowerShell TCP port scanner built on Test-NetConnection and ForEach-Object -Parallel covers most pre-change verification needs without additional tooling. Keep host and port lists in CSV files for reusability, use -InformationLevel Quiet to avoid timeout bottlenecks, and export both CSV and HTML so results integrate with any workflow.

Send-Item -To