diff --git a/playbook/shopfloor-setup/Shopfloor/lib/Invoke-FilteredReportIP.ps1 b/playbook/shopfloor-setup/Shopfloor/lib/Invoke-FilteredReportIP.ps1 new file mode 100644 index 0000000..f1b2b3c --- /dev/null +++ b/playbook/shopfloor-setup/Shopfloor/lib/Invoke-FilteredReportIP.ps1 @@ -0,0 +1,100 @@ +# Invoke-FilteredReportIP.ps1 +# Reports this bay's GE corp IP to the GE Tines webhook with a tight +# subnet filter so PXE LAN (10.9.100.x) or any other 10.x stray address +# never leaks. Mirrors the payload shape of GE's Intune-deployed +# ReportIPAddresses_v2.ps1 so the receiving Tines workflow accepts it. +# +# WHY: +# GE's signed Report IP script filters Get-NetIPAddress with +# $_.StartsWith("10.") - too broad for WJ where PXE LAN is also 10.x. +# Calling our own POST with a narrow filter guarantees the webhook +# only ever sees the bay's AESFMA corp 10.x address. +# +# ALLOWED RANGES (WJ-specific - update if site moves to a new VLAN): +# 10.134.48.0/23 (10.134.48.0 - 10.134.49.255) +# 10.48.249.0/26 (10.48.249.0 - 10.48.249.63) + +param( + [switch]$Force, + [int]$MaxAttempts = 6, + [int]$RetrySeconds = 10 +) + +$ErrorActionPreference = 'Continue' +$logFile = 'C:\Logs\FilteredReportIP.log' +New-Item -ItemType Directory -Path 'C:\Logs' -Force -ErrorAction SilentlyContinue | Out-Null + +function Log([string]$msg) { + $ts = Get-Date -Format 'yyyy-MM-dd HH:mm:ss' + "$ts $msg" | Tee-Object -FilePath $logFile -Append +} + +# Allowed ranges (CIDR -> first IP int + prefix bits) +$allowedRanges = @( + @{ Network = '10.134.48.0'; PrefixLen = 23 }, + @{ Network = '10.48.249.0'; PrefixLen = 26 } +) + +function ConvertTo-Uint32($ip) { + $bytes = ([System.Net.IPAddress]::Parse($ip)).GetAddressBytes() + [Array]::Reverse($bytes) + return [BitConverter]::ToUInt32($bytes, 0) +} + +function Test-InAllowedRange([string]$ip) { + try { + $ipInt = ConvertTo-Uint32 $ip + foreach ($r in $allowedRanges) { + $netInt = ConvertTo-Uint32 $r.Network + $mask = [uint32]([math]::Pow(2, 32) - [math]::Pow(2, 32 - $r.PrefixLen)) + if (($ipInt -band $mask) -eq ($netInt -band $mask)) { return $true } + } + } catch {} + return $false +} + +Log "=== Filtered Report IP fire ===" +$allIPs = Get-NetIPAddress -AddressFamily IPv4 -ErrorAction SilentlyContinue | + Where-Object { $_.IPAddress -notmatch '^169\.254' -and $_.IPAddress -ne '127.0.0.1' } | + Select-Object -ExpandProperty IPAddress +Log "All IPv4 addresses on this bay: $($allIPs -join ', ')" + +$match = @($allIPs | Where-Object { Test-InAllowedRange $_ }) +if ($match.Count -eq 0) { + Log "No IP in allowed ranges (10.134.48.0/23 or 10.48.249.0/26). Not POSTing." + exit 2 +} +$reportIP = $match[0] +Log "Reporting IP: $reportIP" + +$statusFile = "$env:ProgramData\GEA\FilteredReportIP\last-ip.txt" +$lastIP = '' +if (Test-Path $statusFile) { $lastIP = (Get-Content $statusFile -ErrorAction SilentlyContinue) -join '' } +if (-not $Force -and $lastIP -eq $reportIP) { + Log "IP unchanged from last POST ($lastIP). Skipping." + exit 0 +} + +$webhookUrl = 'https://tines.apps.geaerospace.com/webhook/aa891efac08bab3d077d9956ab6614d0/c687048cb0ee35c33760ad14cb9257eb' +$body = @{ + host = $env:COMPUTERNAME + fqdn = "$env:COMPUTERNAME.device.geaerospace.net" + IP = $reportIP + force_update = if ($Force) { 'True' } else { 'False' } +} + +for ($i = 1; $i -le $MaxAttempts; $i++) { + try { + Log "POST attempt $i/$MaxAttempts ..." + $resp = Invoke-WebRequest -Uri $webhookUrl -Method POST -Body $body -TimeoutSec 60 -UseBasicParsing -ErrorAction Stop + Log "POST succeeded. StatusCode=$($resp.StatusCode) Content=$($resp.Content)" + New-Item -ItemType Directory -Path (Split-Path $statusFile) -Force -ErrorAction SilentlyContinue | Out-Null + Set-Content -Path $statusFile -Value $reportIP -Force + exit 0 + } catch { + Log "POST attempt $i failed: $_" + if ($i -lt $MaxAttempts) { Start-Sleep -Seconds $RetrySeconds } + } +} +Log "All $MaxAttempts attempts failed - giving up." +exit 1 diff --git a/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 b/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 index 94a3c4a..5926fb3 100644 --- a/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 +++ b/playbook/shopfloor-setup/Shopfloor/lib/Monitor-IntuneProgress.ps1 @@ -1190,6 +1190,7 @@ try { $lastSync = Get-Date $nextRetrigger = $lastSync.AddMinutes($RetriggerMinutes) + $filteredReportIpSucceeded = $false while ($true) { $snap = Get-Snapshot @@ -1201,6 +1202,20 @@ try { $qrRefreshed = [bool]($qrText -notmatch 'not yet Azure AD joined') } + # Filtered Report IP shim - POSTs this bay's GE corp IP to the + # Tines webhook directly with a tight subnet filter. Fires each + # tick until it succeeds once (script's own status-file dedup + # prevents duplicate POSTs). Bypasses the timing window where + # GE's Intune-deployed Report IP misses (it may run before the + # SCEP cert + AESFMA join land). + if (-not $filteredReportIpSucceeded) { + $filteredScript = Join-Path $PSScriptRoot 'Invoke-FilteredReportIP.ps1' + if (Test-Path $filteredScript) { + $rc = & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $filteredScript 2>&1 | Out-Null + if ($LASTEXITCODE -eq 0) { $filteredReportIpSucceeded = $true } + } + } + Clear-Host Format-Snapshot -Snap $snap -LastSync $lastSync -NextRetrigger $nextRetrigger Write-Host ""