<# .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