Add reboot mode, TLS fallback, kiosk app detection, and user registry scanning

- Add -Reboot/-MinUptimeDays parameter set to reboot high-uptime PCs via API
- Add WebClient fallback for TLS/SSL connection errors on API calls
- Add per-user registry scanning (HKU hives) for installed app detection
- Add Dashboard and Lobby Display kiosk app detection (app IDs 82, 83)
- Add SkipCertificateCheck support for PowerShell 7+
- Increase session timeouts (20s connect, 120s operation)
- Increase default ThrottleLimit from 10 to 25
- Add Install-KioskApp.ps1 and user registry detection test scripts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
cproudlock
2026-02-03 10:44:42 -05:00
parent 8c9dac5e2e
commit f40b79c087
4 changed files with 723 additions and 20 deletions

View File

@@ -35,6 +35,18 @@
$cred = Get-Credential
.\Update-ShopfloorPCs-Remote.ps1 -ComputerName "SHOPFLOOR-PC01" -Credential $cred
.EXAMPLE
# Reboot all PCs with uptime >= 30 days (will prompt for confirmation)
.\Update-ShopfloorPCs-Remote.ps1 -Reboot -MinUptimeDays 30
.EXAMPLE
# Preview which PCs would be rebooted (no actual reboot)
.\Update-ShopfloorPCs-Remote.ps1 -Reboot -MinUptimeDays 30 -WhatIf
.EXAMPLE
# Reboot PCs with uptime >= 60 days without confirmation prompt
.\Update-ShopfloorPCs-Remote.ps1 -Reboot -MinUptimeDays 60 -Force -Credential $cred
.NOTES
Requires:
- WinRM enabled on target PCs (Enable-PSRemoting)
@@ -53,6 +65,15 @@ param(
[Parameter(ParameterSetName='SetupTrust')]
[switch]$SetupTrustedHosts,
[Parameter(ParameterSetName='Reboot', Mandatory)]
[switch]$Reboot,
[Parameter(ParameterSetName='Reboot', Mandatory)]
[int]$MinUptimeDays,
[Parameter(ParameterSetName='Reboot')]
[switch]$Force,
[Parameter()]
[PSCredential]$Credential,
@@ -69,16 +90,36 @@ param(
[switch]$UseSSL,
[Parameter()]
[int]$ThrottleLimit = 10,
[int]$ThrottleLimit = 25,
[Parameter()]
[switch]$WhatIf
)
# SSL/TLS Certificate Bypass for HTTPS connections
# SSL/TLS Configuration - MUST be set before any web requests
# Set ALL modern TLS versions - fixes "underlying connection was closed" errors
try {
if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) {
Add-Type @"
# Enable all available TLS protocols (1.0, 1.1, 1.2, 1.3 if available)
$protocols = [Net.SecurityProtocolType]::Tls12
try { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls13 } catch {}
try { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls11 } catch {}
try { $protocols = $protocols -bor [Net.SecurityProtocolType]::Tls } catch {}
[Net.ServicePointManager]::SecurityProtocol = $protocols
} catch {
# Absolute fallback
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
}
# Increase connection limits and timeouts
[Net.ServicePointManager]::DefaultConnectionLimit = 100
[Net.ServicePointManager]::Expect100Continue = $false
[Net.ServicePointManager]::MaxServicePointIdleTime = 10000
# Certificate Bypass for HTTPS connections (self-signed certs) - PowerShell 5.x
if ($PSVersionTable.PSVersion.Major -lt 7) {
try {
if (-not ([System.Management.Automation.PSTypeName]'TrustAllCertsPolicy').Type) {
Add-Type @"
using System.Net;
using System.Security.Cryptography.X509Certificates;
public class TrustAllCertsPolicy : ICertificatePolicy {
@@ -89,10 +130,22 @@ public class TrustAllCertsPolicy : ICertificatePolicy {
}
}
"@
}
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
} catch {
Write-Warning "Could not set certificate policy: $_"
}
[System.Net.ServicePointManager]::CertificatePolicy = New-Object TrustAllCertsPolicy
} catch { }
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
# Also set callback for newer .NET versions (backup method)
try {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { param($sender, $cert, $chain, $errors) return $true }
} catch {}
}
# Load System.Web for URL encoding (used in WebClient fallback)
try {
Add-Type -AssemblyName System.Web -ErrorAction SilentlyContinue
} catch {}
#region Functions
@@ -115,21 +168,108 @@ function Get-ShopfloorPCsFromApi {
#>
param([string]$ApiUrl)
$fullUrl = "$ApiUrl`?action=getShopfloorPCs"
Write-Log "Querying API: $fullUrl" -Level "INFO"
# Try PowerShell native method first
try {
Write-Log "Querying API: $ApiUrl`?action=getShopfloorPCs" -Level "INFO"
$response = Invoke-RestMethod -Uri "$ApiUrl`?action=getShopfloorPCs" -Method Get -ErrorAction Stop
# Build params - use SkipCertificateCheck for PS7+, rely on ServicePointManager for PS5
$restParams = @{
Uri = $fullUrl
Method = 'Get'
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 30
}
# Add SkipCertificateCheck for PowerShell 7+
if ($PSVersionTable.PSVersion.Major -ge 7) {
$restParams.SkipCertificateCheck = $true
}
$response = Invoke-RestMethod @restParams
if ($response.success -and $response.data) {
Write-Log "API returned $($response.count) shopfloor PCs" -Level "SUCCESS"
# Return full PC objects, not just hostnames
return $response.data
} else {
Write-Log "No shopfloor PCs returned from API" -Level "WARNING"
return @()
}
} catch {
Write-Log "Failed to query API for shopfloor PCs: $_" -Level "ERROR"
$errMsg = $_.Exception.Message
$innerMsg = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { "" }
# Check if it's a TLS/connection error - try WebClient as fallback
if ($errMsg -match "underlying connection|closed|SSL|TLS|secure channel" -or $innerMsg -match "underlying connection|closed|SSL|TLS|secure channel") {
Write-Log "TLS error detected, trying WebClient fallback..." -Level "WARNING"
try {
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("User-Agent", "PowerShell/ShopDB-Updater")
$jsonResponse = $webClient.DownloadString($fullUrl)
$response = $jsonResponse | ConvertFrom-Json
if ($response.success -and $response.data) {
Write-Log "API returned $($response.count) shopfloor PCs (via WebClient)" -Level "SUCCESS"
return $response.data
} else {
Write-Log "No shopfloor PCs returned from API" -Level "WARNING"
return @()
}
} catch {
Write-Log "WebClient fallback also failed: $($_.Exception.Message)" -Level "ERROR"
}
}
Write-Log "Failed to query API for shopfloor PCs: $errMsg" -Level "ERROR"
Write-Log " Exception Type: $($_.Exception.GetType().FullName)" -Level "ERROR"
if ($innerMsg) {
Write-Log " Inner Exception: $innerMsg" -Level "ERROR"
}
Write-Log " Tip: Try using HTTP instead of HTTPS, or check if the server certificate is valid" -Level "INFO"
return @()
}
}
function Get-HighUptimePCsFromApi {
<#
.SYNOPSIS
Queries ShopDB API to get PCs with uptime >= specified days.
#>
param(
[string]$ApiUrl,
[int]$MinUptimeDays
)
$fullUrl = "$ApiUrl`?action=getHighUptimePCs&minUptime=$MinUptimeDays"
Write-Log "Querying API for PCs with uptime >= $MinUptimeDays days: $fullUrl" -Level "INFO"
try {
$restParams = @{
Uri = $fullUrl
Method = 'Get'
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 30
}
if ($PSVersionTable.PSVersion.Major -ge 7) {
$restParams.SkipCertificateCheck = $true
}
$response = Invoke-RestMethod @restParams
if ($response.success -and $response.data) {
Write-Log "API returned $($response.count) PCs with uptime >= $MinUptimeDays days" -Level "SUCCESS"
return $response.data
} else {
Write-Log "No high-uptime PCs returned from API" -Level "WARNING"
return @()
}
} catch {
$errMsg = $_.Exception.Message
Write-Log "Failed to query API for high-uptime PCs: $errMsg" -Level "ERROR"
return @()
}
}
@@ -461,7 +601,8 @@ function Get-RemotePCInfo {
$hasVnc = $false
$regPaths = @(
"HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*"
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
"HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
)
foreach ($path in $regPaths) {
if (Test-Path $path) {
@@ -486,6 +627,8 @@ function Get-RemotePCInfo {
# Get all installed apps once for efficiency (with version info)
$installedApps = @()
$installedAppsWithVersion = @()
# Check machine-wide registry paths
foreach ($path in $regPaths) {
if (Test-Path $path) {
$apps = Get-ItemProperty $path -ErrorAction SilentlyContinue |
@@ -500,6 +643,29 @@ function Get-RemotePCInfo {
}
}
# Check per-user installed apps by enumerating loaded hives in HKU
$hkuKeys = Get-ChildItem "Registry::HKU" -ErrorAction SilentlyContinue
foreach ($key in $hkuKeys) {
$sid = $key.PSChildName
# Only check real user SIDs (S-1-5-21-*), skip _Classes entries
if ($sid -match "^S-1-5-21-" -and $sid -notmatch "_Classes$") {
$userUninstallPath = "Registry::HKU\$sid\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
if (Test-Path $userUninstallPath -ErrorAction SilentlyContinue) {
$apps = Get-ItemProperty $userUninstallPath -ErrorAction SilentlyContinue |
Where-Object { $_.DisplayName -and $_.DisplayName.Trim() -ne "" }
foreach ($app in $apps) {
if ($app.DisplayName -notin $installedApps) {
$installedApps += $app.DisplayName
$installedAppsWithVersion += @{
DisplayName = $app.DisplayName.Trim()
Version = if ($app.DisplayVersion) { $app.DisplayVersion.Trim() } else { "" }
}
}
}
}
}
}
# ================================================================
# Match against tracked applications (embedded from applications.csv)
# ================================================================
@@ -520,6 +686,8 @@ function Get-RemotePCInfo {
@{ app_id = 74; app_name = "DODA"; patterns = @("Dovetail Digital Analysis", "DODA") }
@{ app_id = 75; app_name = "FormStatusMonitor"; patterns = @("FormStatusMonitor") }
@{ app_id = 77; app_name = "HeatTreat"; patterns = @("HeatTreat") }
@{ app_id = 82; app_name = "GE Aerospace Dashboard"; patterns = @("^GE Aerospace Dashboard") }
@{ app_id = 83; app_name = "GE Aerospace Lobby Display"; patterns = @("^GE Aerospace Lobby Display") }
)
$matchedApps = @()
@@ -678,10 +846,36 @@ function Get-RemotePCInfo {
}
$result.HasHeatTreat = $hasHeatTreat
# Dashboard Detection (GE Aerospace Dashboard kiosk installer)
$hasDashboard = $false
foreach ($app in $installedApps) {
if ($app -match "^GE Aerospace Dashboard") {
$hasDashboard = $true
break
}
}
$result.HasDashboard = $hasDashboard
$result.IsDashboard = $hasDashboard
# Lobby Display Detection (GE Aerospace Lobby Display kiosk installer)
$hasLobbyDisplay = $false
foreach ($app in $installedApps) {
if ($app -match "^GE Aerospace Lobby Display") {
$hasLobbyDisplay = $true
break
}
}
$result.HasLobbyDisplay = $hasLobbyDisplay
$result.IsLobbyDisplay = $hasLobbyDisplay
# Determine PC Type based on detected software
# Priority: CMM > Wax Trace > Keyence > EAS1000 > Genspect > Heat Treat > Generic hint > Shopfloor (default)
# Priority: Dashboard > Lobby Display > CMM > Wax Trace > Keyence > EAS1000 > Genspect > Heat Treat > Generic hint > Shopfloor (default)
$detectedPcType = "Shopfloor" # Default for shopfloor PCs
if ($result.IsCMM) {
if ($hasDashboard) {
$detectedPcType = "Dashboard"
} elseif ($hasLobbyDisplay) {
$detectedPcType = "Lobby Display"
} elseif ($result.IsCMM) {
$detectedPcType = "CMM"
} elseif ($result.IsWaxTrace) {
$detectedPcType = "Wax Trace"
@@ -776,8 +970,23 @@ function Send-PCDataToApi {
$postData.installedApps = $PCData.MatchedAppsJson
}
# Build params for API call
$restParams = @{
Uri = $ApiUrl
Method = 'Post'
Body = $postData
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 30
}
# Add SkipCertificateCheck for PowerShell 7+
if ($PSVersionTable.PSVersion.Major -ge 7) {
$restParams.SkipCertificateCheck = $true
}
# Send to API
$response = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $postData -ErrorAction Stop
$response = Invoke-RestMethod @restParams
return @{
Success = $response.success
@@ -785,9 +994,37 @@ function Send-PCDataToApi {
MachineId = $response.machineid
}
} catch {
$errMsg = $_.Exception.Message
$innerMsg = if ($_.Exception.InnerException) { $_.Exception.InnerException.Message } else { "" }
# Try WebClient fallback for TLS errors
if ($errMsg -match "underlying connection|closed|SSL|TLS|secure channel" -or $innerMsg -match "underlying connection|closed|SSL|TLS|secure channel") {
try {
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("Content-Type", "application/x-www-form-urlencoded")
$webClient.Headers.Add("User-Agent", "PowerShell/ShopDB-Updater")
# Convert postData hashtable to form-urlencoded string
$formData = ($postData.GetEnumerator() | ForEach-Object {
"$([System.Web.HttpUtility]::UrlEncode($_.Key))=$([System.Web.HttpUtility]::UrlEncode($_.Value))"
}) -join "&"
$jsonResponse = $webClient.UploadString($ApiUrl, $formData)
$response = $jsonResponse | ConvertFrom-Json
return @{
Success = $response.success
Message = $response.message
MachineId = $response.machineid
}
} catch {
# WebClient also failed
}
}
return @{
Success = $false
Message = $_.Exception.Message
Message = if ($innerMsg) { "$errMsg ($innerMsg)" } else { $errMsg }
MachineId = $null
}
}
@@ -815,6 +1052,121 @@ if ($SetupTrustedHosts) {
exit 0
}
# Handle Reboot mode
if ($Reboot) {
Write-Log "=== Reboot Mode ===" -Level "INFO"
Write-Log "Minimum Uptime: $MinUptimeDays days" -Level "INFO"
Write-Log "API URL: $ApiUrl" -Level "INFO"
# Query API for high-uptime PCs
$highUptimePCs = Get-HighUptimePCsFromApi -ApiUrl $ApiUrl -MinUptimeDays $MinUptimeDays
if ($highUptimePCs.Count -eq 0) {
Write-Log "No PCs found with uptime >= $MinUptimeDays days" -Level "WARNING"
exit 0
}
# Display PCs to be rebooted
Write-Host ""
Write-Host "=" * 80 -ForegroundColor Red
Write-Host " WARNING: The following $($highUptimePCs.Count) PCs will be REBOOTED" -ForegroundColor Red
Write-Host "=" * 80 -ForegroundColor Red
Write-Host ""
Write-Host (" {0,-25} {1,-15} {2,-12} {3,-20}" -f "Hostname", "Machine #", "Uptime Days", "Last Boot") -ForegroundColor Yellow
Write-Host " $("-" * 75)" -ForegroundColor Gray
foreach ($pc in $highUptimePCs) {
$machineNo = if ($pc.machinenumber) { $pc.machinenumber } else { "-" }
$uptimeDays = if ($pc.uptime_days) { $pc.uptime_days } else { "?" }
$lastBoot = if ($pc.lastboottime) { $pc.lastboottime } else { "Unknown" }
Write-Host (" {0,-25} {1,-15} {2,-12} {3,-20}" -f $pc.hostname, $machineNo, $uptimeDays, $lastBoot)
}
Write-Host " $("-" * 75)" -ForegroundColor Gray
Write-Host ""
if ($WhatIf) {
Write-Log "WhatIf mode - no reboots will be performed" -Level "WARNING"
Write-Host " Would reboot $($highUptimePCs.Count) PCs" -ForegroundColor Yellow
exit 0
}
# Confirmation
if (-not $Force) {
Write-Host " Are you sure you want to reboot these $($highUptimePCs.Count) PCs?" -ForegroundColor Red
Write-Host ""
$confirm = Read-Host " Type 'YES' to confirm"
if ($confirm -ne "YES") {
Write-Log "Reboot cancelled by user" -Level "WARNING"
exit 0
}
Write-Host ""
}
# Prompt for credentials if not provided
if (-not $Credential) {
Write-Log "No credentials provided. Prompting for credentials..." -Level "INFO"
$Credential = Get-Credential -Message "Enter admin credentials for remote PCs"
if (-not $Credential) {
Write-Log "Credentials required. Exiting." -Level "ERROR"
exit 1
}
}
Write-Log "Starting reboot process..." -Level "INFO"
# Build FQDN list
$targetPCs = @()
foreach ($pc in $highUptimePCs) {
$fqdn = if ($pc.hostname -like "*.*") { $pc.hostname } else { "$($pc.hostname).$DnsSuffix" }
$targetPCs += @{
Hostname = $pc.hostname
FQDN = $fqdn
UptimeDays = $pc.uptime_days
}
}
# Reboot each PC
$rebootSuccess = 0
$rebootFailed = 0
foreach ($target in $targetPCs) {
Write-Log "Rebooting $($target.Hostname) (uptime: $($target.UptimeDays) days)..." -Level "INFO"
try {
$rebootParams = @{
ComputerName = $target.FQDN
Force = $true
ErrorAction = 'Stop'
}
if ($Credential) {
$rebootParams.Credential = $Credential
}
Restart-Computer @rebootParams
Write-Log " -> Reboot command sent successfully" -Level "SUCCESS"
$rebootSuccess++
} catch {
Write-Log " -> FAILED: $($_.Exception.Message)" -Level "ERROR"
$rebootFailed++
}
}
# Summary
Write-Host ""
Write-Host "=" * 60 -ForegroundColor Cyan
Write-Host " REBOOT SUMMARY" -ForegroundColor Cyan
Write-Host "=" * 60 -ForegroundColor Cyan
Write-Host ""
Write-Host " Total PCs: $($targetPCs.Count)" -ForegroundColor White
Write-Host " Successful: $rebootSuccess" -ForegroundColor Green
Write-Host " Failed: $rebootFailed" -ForegroundColor $(if ($rebootFailed -gt 0) { "Red" } else { "White" })
Write-Host ""
Write-Host "=" * 60 -ForegroundColor Cyan
exit 0
}
Write-Log "=== ShopDB Remote PC Update Script ===" -Level "INFO"
Write-Log "API URL: $ApiUrl" -Level "INFO"
Write-Log "DNS Suffix: $DnsSuffix" -Level "INFO"
@@ -919,7 +1271,8 @@ if ($WhatIf) {
$fqdnList = $targetComputers | ForEach-Object { $_.FQDN }
# Create session options with short timeout (15 seconds per PC)
$sessionOption = New-PSSessionOption -OpenTimeout 15000 -OperationTimeout 30000 -NoMachineProfile
# Timeouts: 20s to connect, 120s for operations (app detection takes time)
$sessionOption = New-PSSessionOption -OpenTimeout 20000 -OperationTimeout 120000 -NoMachineProfile
$sessionParams = @{
ComputerName = $fqdnList
@@ -973,6 +1326,8 @@ foreach ($result in $results) {
# Show detected PC type and software
if ($result.DetectedPcType) {
$detectedSoftware = @()
if ($result.HasDashboard) { $detectedSoftware += "GE Aerospace Dashboard" }
if ($result.HasLobbyDisplay) { $detectedSoftware += "GE Aerospace Lobby Display" }
if ($result.HasPcDmis) { $detectedSoftware += "PC-DMIS" }
if ($result.HasGoCMM) { $detectedSoftware += "goCMM" }
if ($result.HasDODA) { $detectedSoftware += "DODA" }
@@ -1011,7 +1366,18 @@ foreach ($result in $results) {
hostname = $result.Hostname
hasWinRM = "1"
}
$winrmResponse = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $winrmBody -ErrorAction Stop
$winrmParams = @{
Uri = $ApiUrl
Method = 'Post'
Body = $winrmBody
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 15
}
if ($PSVersionTable.PSVersion.Major -ge 7) {
$winrmParams.SkipCertificateCheck = $true
}
$winrmResponse = Invoke-RestMethod @winrmParams
if ($winrmResponse.success) {
Write-Log " -> WinRM status updated" -Level "SUCCESS"
}
@@ -1071,7 +1437,18 @@ if ($connectionFailures.Count -gt 0) {
action = "getRecordedIP"
hostname = $hostname
}
$ipResponse = Invoke-RestMethod -Uri $ApiUrl -Method Post -Body $ipLookupBody -ErrorAction Stop
$ipLookupParams = @{
Uri = $ApiUrl
Method = 'Post'
Body = $ipLookupBody
ErrorAction = 'Stop'
UseBasicParsing = $true
TimeoutSec = 15
}
if ($PSVersionTable.PSVersion.Major -ge 7) {
$ipLookupParams.SkipCertificateCheck = $true
}
$ipResponse = Invoke-RestMethod @ipLookupParams
if ($ipResponse.success -and $ipResponse.ipaddress -and $ipResponse.ipaddress -match "^10\.134\.") {
$fallbackIP = $ipResponse.ipaddress