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>
This commit is contained in:
266
remote-execution/Find-ShopfloorPCs.ps1
Normal file
266
remote-execution/Find-ShopfloorPCs.ps1
Normal file
@@ -0,0 +1,266 @@
|
||||
<#
|
||||
.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
|
||||
Reference in New Issue
Block a user