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 Quietin 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 insideForEach-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.


