Files
powershell-scripts/remote-execution/Find-ShopfloorPCs.ps1
cproudlock 847ec402bd Add scheduled maintenance, PC list export, and subnet scanner scripts
- Schedule-Maintenance.ps1: DPAPI credential storage + Task Scheduler integration
- Export-PCList.ps1: Pull PC lists from ShopDB API with type/BU filtering
- Find-ShopfloorPCs.ps1: Parallel subnet scanner with WinRM and DNS checks
- INSTRUCTIONS.txt: Schedule-Maintenance.ps1 documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 14:58:18 -05:00

267 lines
9.3 KiB
PowerShell

<#
.SYNOPSIS
Scans a subnet for live hosts and tests WinRM connectivity to build a
usable computer list for Invoke-RemoteMaintenance.ps1.
.DESCRIPTION
1. Pings all IPs in the specified subnet to find responsive hosts.
2. Tests WinRM (TCP 5985) on each responsive host.
3. Attempts to resolve hostnames via reverse DNS.
4. Outputs a text file (one host per line) compatible with
Invoke-RemoteMaintenance.ps1 -ComputerListFile.
.PARAMETER Subnet
CIDR notation subnet to scan (default: 10.134.48.0/23).
.PARAMETER OutputFile
Path for the generated computer list (default: shopfloor-pcs.txt in script dir).
.PARAMETER PingTimeout
Ping timeout in milliseconds (default: 500).
.PARAMETER WinRMPort
WinRM port to test (default: 5985).
.PARAMETER WinRMTimeout
TCP connection timeout in milliseconds for WinRM test (default: 1000).
.PARAMETER ThrottleLimit
Max concurrent ping/test jobs (default: 50).
.PARAMETER SkipWinRM
Only do ping sweep, skip WinRM test.
.EXAMPLE
.\Find-ShopfloorPCs.ps1
# Scans 10.134.48.0/23, outputs shopfloor-pcs.txt
.EXAMPLE
.\Find-ShopfloorPCs.ps1 -Subnet "10.134.48.0/23" -OutputFile "C:\temp\pcs.txt"
.EXAMPLE
.\Find-ShopfloorPCs.ps1 -SkipWinRM
# Ping sweep only, no WinRM check
#>
[CmdletBinding()]
param(
[string]$Subnet = "10.134.48.0/23",
[string]$OutputFile,
[int]$PingTimeout = 500,
[int]$WinRMPort = 5985,
[int]$WinRMTimeout = 1000,
[int]$ThrottleLimit = 50,
[switch]$SkipWinRM
)
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
function Write-Status {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "HH:mm:ss"
$color = switch ($Level) {
"OK" { "Green" }
"WARN" { "Yellow" }
"ERROR" { "Red" }
"SKIP" { "DarkGray"}
default { "Cyan" }
}
Write-Host "[$timestamp] [$Level] $Message" -ForegroundColor $color
}
function ConvertFrom-CIDR {
<# Returns an array of IP address strings for the given CIDR block.
Excludes network and broadcast addresses. #>
param([string]$CIDR)
$parts = $CIDR -split '/'
$networkIP = [System.Net.IPAddress]::Parse($parts[0])
$prefixLen = [int]$parts[1]
$ipBytes = $networkIP.GetAddressBytes()
# .NET gives bytes in network order (big-endian)
[Array]::Reverse($ipBytes)
$ipUint = [BitConverter]::ToUInt32($ipBytes, 0)
$hostBits = 32 - $prefixLen
$numAddresses = [math]::Pow(2, $hostBits)
$networkAddr = $ipUint -band ([uint32]::MaxValue -shl $hostBits)
$ips = [System.Collections.Generic.List[string]]::new()
# Skip .0 (network) and last (broadcast)
for ($i = 1; $i -lt ($numAddresses - 1); $i++) {
$addr = $networkAddr + $i
$bytes = [BitConverter]::GetBytes([uint32]$addr)
[Array]::Reverse($bytes)
$ips.Add(("{0}.{1}.{2}.{3}" -f $bytes[0], $bytes[1], $bytes[2], $bytes[3]))
}
return $ips
}
# ---------------------------------------------------------------------------
# Default output path
# ---------------------------------------------------------------------------
if (-not $OutputFile) {
$OutputFile = Join-Path $PSScriptRoot "shopfloor-pcs.txt"
}
# ---------------------------------------------------------------------------
# Phase 1: Generate IP list
# ---------------------------------------------------------------------------
Write-Status "Generating IP list for $Subnet"
$allIPs = ConvertFrom-CIDR -CIDR $Subnet
Write-Status "$($allIPs.Count) host addresses to scan"
# ---------------------------------------------------------------------------
# Phase 2: Ping sweep (parallel)
# ---------------------------------------------------------------------------
Write-Status "Starting ping sweep (timeout: ${PingTimeout}ms, threads: $ThrottleLimit)..."
$pingResults = $allIPs | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$ip = $_
$ping = New-Object System.Net.NetworkInformation.Ping
try {
$reply = $ping.Send($ip, $using:PingTimeout)
if ($reply.Status -eq 'Success') {
[PSCustomObject]@{
IP = $ip
Alive = $true
RoundTrip = $reply.RoundtripTime
}
}
} catch {
# Silently skip unreachable hosts
} finally {
$ping.Dispose()
}
}
$aliveHosts = @($pingResults | Where-Object { $_.Alive })
Write-Status "$($aliveHosts.Count) hosts responded to ping" -Level "OK"
if ($aliveHosts.Count -eq 0) {
Write-Status "No hosts found. Check subnet and network connectivity." -Level "ERROR"
exit 1
}
# ---------------------------------------------------------------------------
# Phase 3: WinRM port test (parallel)
# ---------------------------------------------------------------------------
if ($SkipWinRM) {
Write-Status "Skipping WinRM test (-SkipWinRM)" -Level "SKIP"
$winrmHosts = $aliveHosts
} else {
Write-Status "Testing WinRM (port $WinRMPort) on $($aliveHosts.Count) hosts..."
$winrmResults = $aliveHosts | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$ip = $_.IP
$roundTrip = $_.RoundTrip
$port = $using:WinRMPort
$timeout = $using:WinRMTimeout
$tcp = New-Object System.Net.Sockets.TcpClient
try {
$task = $tcp.ConnectAsync($ip, $port)
$connected = $task.Wait($timeout)
if ($connected -and $tcp.Connected) {
[PSCustomObject]@{
IP = $ip
WinRM = $true
RoundTrip = $roundTrip
}
}
} catch {
# Port closed or filtered
} finally {
$tcp.Dispose()
}
}
$winrmHosts = @($winrmResults | Where-Object { $_.WinRM })
$noWinRM = $aliveHosts.Count - $winrmHosts.Count
Write-Status "$($winrmHosts.Count) hosts have WinRM open, $noWinRM alive but no WinRM" -Level "OK"
}
if ($winrmHosts.Count -eq 0) {
Write-Status "No hosts with WinRM found." -Level "ERROR"
exit 1
}
# ---------------------------------------------------------------------------
# Phase 4: Reverse DNS resolution
# ---------------------------------------------------------------------------
Write-Status "Resolving hostnames via reverse DNS..."
$resolvedHosts = $winrmHosts | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel {
$ip = $_.IP
$roundTrip = $_.RoundTrip
$hostname = $null
try {
$entry = [System.Net.Dns]::GetHostEntry($ip)
if ($entry.HostName -and $entry.HostName -ne $ip) {
$hostname = $entry.HostName
}
} catch {
# No reverse DNS
}
[PSCustomObject]@{
IP = $ip
Hostname = $hostname
RoundTrip = $roundTrip
}
}
$withDNS = @($resolvedHosts | Where-Object { $_.Hostname })
$withoutDNS = @($resolvedHosts | Where-Object { -not $_.Hostname })
Write-Status "$($withDNS.Count) resolved via DNS, $($withoutDNS.Count) IP-only" -Level "OK"
# ---------------------------------------------------------------------------
# Phase 5: Write output file
# ---------------------------------------------------------------------------
$sortedHosts = $resolvedHosts | Sort-Object { [Version]($_.IP -replace '(\d+)\.(\d+)\.(\d+)\.(\d+)', '$1.$2.$3.$4') }
$lines = [System.Collections.Generic.List[string]]::new()
$lines.Add("# Shopfloor PC List - Auto-generated $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')")
$lines.Add("# Subnet: $Subnet")
$lines.Add("# Hosts scanned: $($allIPs.Count) | Alive: $($aliveHosts.Count) | WinRM: $($winrmHosts.Count)")
$lines.Add("# Usage: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot")
$lines.Add("#")
$lines.Add("# Format: hostname or IP (one per line). Lines starting with # are ignored.")
$lines.Add("")
foreach ($h in $sortedHosts) {
if ($h.Hostname) {
# Use short hostname (strip domain suffix) as the entry, comment with IP
$shortName = ($h.Hostname -split '\.')[0]
$lines.Add("$shortName # $($h.IP) ($($h.RoundTrip)ms)")
} else {
$lines.Add("$($h.IP) # no DNS ($($h.RoundTrip)ms)")
}
}
$lines | Out-File -FilePath $OutputFile -Encoding UTF8
Write-Status "Wrote $($winrmHosts.Count) entries to: $OutputFile" -Level "OK"
# ---------------------------------------------------------------------------
# Summary table
# ---------------------------------------------------------------------------
Write-Host ""
Write-Host "=== SCAN RESULTS ===" -ForegroundColor White
Write-Host ("{0,-30} {1,-18} {2,8}" -f "HOSTNAME", "IP", "PING(ms)") -ForegroundColor DarkGray
Write-Host ("-" * 58) -ForegroundColor DarkGray
foreach ($h in $sortedHosts) {
$name = if ($h.Hostname) { ($h.Hostname -split '\.')[0] } else { "(no DNS)" }
Write-Host ("{0,-30} {1,-18} {2,8}" -f $name, $h.IP, $h.RoundTrip)
}
Write-Host ""
Write-Host "Output file: $OutputFile" -ForegroundColor Green
Write-Host "Next step: .\Invoke-RemoteMaintenance.ps1 -ComputerListFile `"$OutputFile`" -Task Reboot" -ForegroundColor Yellow