
How to use PowerShell to find the living machines and the listening services, and then document the carnage
You run a network. People lie about what is online. Firewalls pretend to be polite. Your job is to stop believing statements and start believing signals. Ping sweeps and port scans do what polite questions will not: they expose truth. Do this only on networks you own or where you have written permission. If you go poking strangers you will get a call from someone in an ugly suit and you will deserve it.
Permission and simple ethics
Get permission in writing. Schedule scans in a maintenance window. Tell the helpdesk and the managers. Tag your scans in the CMDB. If the IDS starts throwing tantrums, own it. If you are scanning without authorization, stop. Get an actual job.
What this piece fixes
You asked for lean scripts that do not lie to you. These scripts:
- Log everything so you can be accused with proof instead of rumor.
- Stop treating every failure as the same. DNS failure is not the same as a filtered SYN.
- Use TCP fallback for liveness when ICMP is blocked.
- Handle /32 and /31 CIDR without returning nonsense broadcast addresses.
- Are triage tools. For deep fingerprinting use the proper tools like nmap in a lab.
How to read these scripts
Copy the small helpers into the same folder and run from PowerShell. PowerShell 7+ gives concurrency and speed. PowerShell 5.1 will still work but slower. Throttle is not a suggestion. Start conservative. Your switches and IDS will thank you.
Helpers and logging
Save this as Helpers.ps1. It resolves names, writes an append-only log, and gives you a place to scream into when things go wrong.
# Helpers.ps1
param([string] $LogFile = ".\scan.log")
function Write-Log {
param($Message, [string] $Level = "INFO")
$ts = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
$line = "$ts [$Level] $Message"
$line
$line | Out-File -FilePath $LogFile -Append -Encoding utf8
}
function Safe-Resolve {
param([string] $Name)
try {
$ip = [System.Net.Dns]::GetHostAddresses($Name) |
Where-Object { $_.AddressFamily -eq 'InterNetwork' } |
Select-Object -First 1
if ($null -eq $ip) { throw "No IPv4" }
return $ip.IPAddressToString
} catch {
Write-Log "DNS resolution failed for $Name : $_" "WARN"
return $null
}
}
Ping sweep that does not pretend silence means death
Save as PingSweepWithFallback.ps1. It pings. If ICMP is blocked it tries common TCP ports to decide if the host is alive. It respects /31 and /32 and logs what it found.
# PingSweepWithFallback.ps1
param(
[string[]] $Targets,
[int] $IcmpTimeoutMs = 800,
[int] $TcpFallbackTimeoutMs = 800,
[int[]] $FallbackPorts = @(80,443),
[string] $LogFile = ".\ping-sweep.log",
[int] $Throttle = 50
)
. .\Helpers.ps1 -LogFile $LogFile
function Expand-Cidr {
param([string] $cidr)
$parts = $cidr.Split('/')
if ($parts.Count -ne 2) { throw "Invalid CIDR $cidr" }
$baseIp = [System.Net.IPAddress]::Parse($parts[0]).GetAddressBytes(); [Array]::Reverse($baseIp)
$ipInt = [BitConverter]::ToUInt32($baseIp,0); $mask = [int]$parts[1]
if ($mask -gt 32 -or $mask -lt 0) { throw "Bad mask $mask" }
if ($mask -eq 32) {
$bytes = [BitConverter]::GetBytes([uint32]$ipInt); [Array]::Reverse($bytes)
return @([System.Net.IPAddress]::New($bytes).ToString())
}
if ($mask -eq 31) {
$count = 2
$first = $ipInt -band (-bor 0xFFFFFFFF -shl (32 - $mask))
$ips = for ($i=0; $i -lt $count; $i++) {
$a = $first + $i; $b=[BitConverter]::GetBytes([uint32]$a); [Array]::Reverse($b)
([System.Net.IPAddress]::New($b)).ToString()
}
return $ips
}
$hostCount = [math]::Pow(2, 32 - $mask) - 2
if ($hostCount -le 0) { return @() }
$network = $ipInt -band (-bor 0xFFFFFFFF -shl (32 - $mask))
$ips = for ($i=1; $i -le $hostCount; $i++) {
$a = $network + $i; $b=[BitConverter]::GetBytes([uint32]$a); [Array]::Reverse($b)
([System.Net.IPAddress]::New($b)).ToString()
}
return $ips
}
function Test-TcpPortDetailed {
param([string] $Host, [int] $Port, [int] $TimeoutMs = 1000, [string] $LogFile = ".\port-scan.log")
$res = [PSCustomObject]@{ Host=$Host; Port=$Port; Open=$false; ErrorMessage=$null; ErrorType=$null; DurationMs=$null }
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
$client = New-Object System.Net.Sockets.TcpClient
$iar = $client.BeginConnect($Host, $Port, $null, $null)
if (-not $iar.AsyncWaitHandle.WaitOne($TimeoutMs)) {
$client.Close()
$res.ErrorMessage = "Timeout"
$res.ErrorType = "Timeout"
return $res
}
$client.EndConnect($iar)
$res.Open = $client.Connected
$client.Close()
} catch [System.Net.Sockets.SocketException] {
$res.ErrorMessage = $_.Exception.Message
$res.ErrorType = "SocketException"
} catch {
$res.ErrorMessage = $_.Exception.Message
$res.ErrorType = "Other"
} finally {
$sw.Stop(); $res.DurationMs = $sw.ElapsedMilliseconds
"$((Get-Date).ToString('s')) [DEBUG] Test-TcpPortDetailed $($Host):$($Port) Open=$($res.Open) Err=$($res.ErrorType) Msg='$($res.ErrorMessage)'" | Out-File -FilePath $LogFile -Append -Encoding utf8
}
return $res
}
function Test-Host-Alive {
param($ip, $icmpTimeoutMs, $tcpTimeoutMs, $fallbackPorts)
$icmp = $false
try {
$icmp = Test-Connection -ComputerName $ip -Count 1 -Quiet -TimeoutMilliseconds $icmpTimeoutMs
} catch {
Write-Log "ICMP test failed for $ip : $_" "DEBUG"
$icmp = $false
}
if ($icmp) { return @{ IP=$ip; Alive=$true; Method="ICMP" } }
foreach ($p in $fallbackPorts) {
$tcp = Test-TcpPortDetailed -Host $ip -Port $p -TimeoutMs $tcpTimeoutMs -LogFile $LogFile
if ($tcp.Open) { return @{ IP=$ip; Alive=$true; Method="TCP($p)"; Details=$tcp } }
}
return @{ IP=$ip; Alive=$false; Method="None" }
}
$ipList = @()
foreach ($t in $Targets) {
if ($t -match '/') { $ipList += Expand-Cidr $t }
elseif (Test-Path $t) { $ipList += (Get-Content $t) | Where-Object { $_ -match '\d+\.\d+\.\d+\.\d+' } }
else {
$resolved = Safe-Resolve $t
if ($resolved) { $ipList += $resolved } else { Write-Log "Could not resolve $t" "WARN" }
}
}
$ipList = $ipList | Select-Object -Unique
Write-Log "Starting ping sweep on $($ipList.Count) addresses" "INFO"
$results = @()
if ($PSVersionTable.PSVersion.Major -ge 7) {
$results = $ipList | ForEach-Object -Parallel {
param($ip,$IcmpTimeoutMs,$TcpFallbackTimeoutMs,$FallbackPorts,$LogFile)
. .\Helpers.ps1 -LogFile $LogFile
Test-Host-Alive -ip $ip -icmpTimeoutMs $IcmpTimeoutMs -tcpTimeoutMs $TcpFallbackTimeoutMs -fallbackPorts $FallbackPorts
} -ThrottleLimit $Throttle -ArgumentList $IcmpTimeoutMs,$TcpFallbackTimeoutMs,$FallbackPorts,$LogFile
} else {
foreach ($ip in $ipList) {
$results += Test-Host-Alive -ip $ip -icmpTimeoutMs $IcmpTimeoutMs -tcpTimeoutMs $TcpFallbackTimeoutMs -fallbackPorts $FallbackPorts
}
}
$results | Where-Object { $_.Alive -eq $true } | Sort-Object IP
Write-Log "Ping sweep complete. Live hosts: $((($results | Where-Object {$_.Alive}) | Measure-Object).Count)" "INFO"
Port scan that reports why things failed
Save as PortHelpers.ps1. Use it when you need reasons, not excuses.
# PortHelpers.ps1
param([string] $LogFile = ".\port-scan.log")
function Test-TcpPortDetailed {
param(
[Parameter(Mandatory)] [string] $Host,
[Parameter(Mandatory)] [int] $Port,
[int] $TimeoutMs = 1000,
[string] $LogFile = ".\port-scan.log"
)
$res = [PSCustomObject]@{ Host=$Host; Port=$Port; Open=$false; ErrorMessage=$null; ErrorType=$null; DurationMs=$null }
$sw = [System.Diagnostics.Stopwatch]::StartNew()
try {
$client = New-Object System.Net.Sockets.TcpClient
$iar = $client.BeginConnect($Host, $Port, $null, $null)
if (-not $iar.AsyncWaitHandle.WaitOne($TimeoutMs)) {
$client.Close()
$res.ErrorMessage = "Timeout"
$res.ErrorType = "Timeout"
return $res
}
$client.EndConnect($iar)
$res.Open = $client.Connected
$client.Close()
} catch [System.Net.Sockets.SocketException] {
$res.ErrorMessage = $_.Exception.Message
$res.ErrorType = "SocketException"
} catch {
$res.ErrorMessage = $_.Exception.Message
$res.ErrorType = "Other"
} finally {
$sw.Stop(); $res.DurationMs = $sw.ElapsedMilliseconds
"$((Get-Date).ToString('s')) [DEBUG] Test-TcpPortDetailed $($Host):$($Port) Open=$($res.Open) Err=$($res.ErrorType) Msg='$($res.ErrorMessage)'" | Out-File -FilePath $LogFile -Append -Encoding utf8
}
return $res
}
function PortScan {
param([string[]] $Targets, [int[]] $Ports = @(22,80,443,3389), [int] $TimeoutMs = 1000, [int] $Throttle = 50, [string] $LogFile = ".\port-scan.log")
. .\Helpers.ps1 -LogFile $LogFile
Write-Log "Starting port scan for $($Targets.Count) hosts and $($Ports.Count) ports" "INFO"
$out = @()
if ($PSVersionTable.PSVersion.Major -ge 7) {
$out = $Targets | ForEach-Object -Parallel {
param($host,$ports,$TimeoutMs,$LogFile)
. .\PortHelpers.ps1 -LogFile $LogFile
foreach ($p in $ports) {
Test-TcpPortDetailed -Host $host -Port $p -TimeoutMs $TimeoutMs -LogFile $LogFile
}
} -ThrottleLimit $Throttle -ArgumentList $Ports,$TimeoutMs,$LogFile
} else {
foreach ($h in $Targets) {
foreach ($p in $Ports) {
$out += Test-TcpPortDetailed -Host $h -Port $p -TimeoutMs $TimeoutMs -LogFile $LogFile
}
}
}
Write-Log "Port scan complete. Open ports found: $((($out | Where-Object {$_.Open}) | Measure-Object).Count)" "INFO"
return $out
}
CSV driven scan that does not hide failures
Put your inventory in assets.csv with headers Name,IP,Location. This script runs the ping sweep with TCP fallback and then scans live hosts. It writes a scan-report.csv with Alive and OpenPorts so you can make tickets and stop improvising.
# scan-from-csv-enhanced.ps1
param(
[string] $InputCsv = ".\assets.csv",
[int[]] $Ports = @(22,80,443),
[int] $IcmpTimeoutMs = 800,
[int] $PortTimeoutMs = 800,
[string] $LogFile = ".\scan.log",
[int] $Throttle = 50
)
. .\Helpers.ps1 -LogFile $LogFile
. .\PingSweepWithFallback.ps1 -LogFile $LogFile -Throttle $Throttle
. .\PortHelpers.ps1 -LogFile $LogFile
if (-not (Test-Path $InputCsv)) { Write-Log "CSV $InputCsv not found" "ERROR"; exit 1 }
$assets = Import-Csv $InputCsv
$targets = $assets | Select-Object -ExpandProperty IP -Unique
Write-Log "Scanning assets from $InputCsv count=$($targets.Count)" "INFO"
$live = & .\PingSweepWithFallback.ps1 -Targets $targets -IcmpTimeoutMs $IcmpTimeoutMs -TcpFallbackTimeoutMs $PortTimeoutMs -FallbackPorts $Ports -LogFile $LogFile -Throttle $Throttle
$liveIPs = $live | Where-Object { $_.Alive } | Select-Object -ExpandProperty IP
$scan = PortScan -Targets $liveIPs -Ports $Ports -TimeoutMs $PortTimeoutMs -Throttle $Throttle -LogFile $LogFile
$report = foreach ($row in $assets) {
$ip = $row.IP
$aliveRow = $live | Where-Object { $_.IP -eq $ip } | Select-Object -First 1
$open = ($scan | Where-Object { $_.Host -eq $ip -and $_.Open } | ForEach-Object { $_.Port }) -join ','
[PSCustomObject]@{
Name = $row.Name
IP = $ip
Location = $row.Location
Alive = $aliveRow.Alive
Method = $aliveRow.Method
OpenPorts = $open
}
}
$report | Export-Csv -Path "scan-report.csv" -NoTypeInformation
$report | Format-Table -AutoSize
Write-Log "Report saved to scan-report.csv" "INFO"
How to run this without making enemies
Open PowerShell in the folder with the scripts. Run the CSV script with the right file. Use low throttle until you know your switches will not cry. Sample command to run the CSV scan:
pwsh -File .\scan-from-csv-enhanced.ps1 -InputCsv .\assets.csv -Ports @(22,80,443) -Throttle 40
Interpreting results without invented narratives
No ICMP does not always mean dead. Timeout can mean a filter. Connection refused means the host answered and said no to that port. Socket exception or DNS failure means fix DNS first. Look at the logs. Correlate with firewall and IDS alerts. Create a ticket for open RDP and SMB ports. Patch, segment, or justify. Repeat on a schedule.
When to stop and use better tools
These scripts are for triage, inventory, and remediation tickets. When you need banners, versions, OS guesses, or safe NSE scripts, use nmap in a lab with permission. When you build a persistent inventory system, use a maintained IP library for network math and an established logging backend. Do not pretend homegrown equals professional.
Operational rules for people who still care about jobs
Log everything. Tag scans in the CMDB. Notify helpdesk and ops windows. Limit concurrency to what your network can bear. Run during maintenance windows. If an executive asks if you can do it quietly, say no and mean it. Quiet scanning is a lie in most corporate networks.