# Autopilot Monitor Bootstrap Guard Test # Dry-run only: does not exit, does not install, only reports what WOULD happen. # Mirrors Install-AutopilotMonitor.ps1 (v2.7) incl. the relax exemption for # guards 2+3, triggered by "Windows Backup for Organizations" (OOBE InProgress) # or a positively identified Windows 365 Cloud PC, the guard 4 (uptime) # exemption while the Cloud PC relax is active, and the agent check. param( [string]$AgentBinPath = 'C:\ProgramData\AutopilotMonitor\Agent', [int]$OobeProfileMaxAgeMinutes = 15 ) $ErrorActionPreference = 'SilentlyContinue' function Write-Step { param( [string]$Status, [string]$Message ) $prefix = switch ($Status) { 'PASS' { '[PASS]' } 'SKIP' { '[SKIP]' } 'WARN' { '[WARN]' } 'INFO' { '[INFO]' } default { '[....]' } } Write-Host "$prefix $Message" } # HKLM through the 64-bit view, as the installer reads it: a 32-bit host would read WOW6432Node. function Get-RegistryValueSafe { param( [string]$SubKey, [string]$Name ) try { $hive = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) try { $key = $hive.OpenSubKey($SubKey) if ($null -eq $key) { return $null } try { return $key.GetValue($Name) } finally { $key.Close() } } finally { $hive.Close() } } catch { return $null } } function Test-RegistryKeySafe { param([string]$SubKey) try { $hive = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Registry64) try { $key = $hive.OpenSubKey($SubKey) if ($null -eq $key) { return $false } $key.Close() return $true } finally { $hive.Close() } } catch { return $false } } # Positive OOBE check via WinRT Windows.System.Profile.SystemSetupInfo.OutOfBoxExperienceState. # Requires Windows 10, version 1809 (10.0.17763.0). Older builds / errors -> $false (SKIP-safe). # Docs: https://learn.microsoft.com/en-us/uwp/api/windows.system.profile.systemsetupinfo function Test-OobeInProgress { try { $null = [Windows.System.Profile.SystemSetupInfo, Windows.System.Profile, ContentType = WindowsRuntime] return ([Windows.System.Profile.SystemSetupInfo]::OutOfBoxExperienceState).ToString() -eq 'InProgress' } catch { return $false } } # Positive Windows 365 Cloud PC detection: two local markers, BOTH required (AND), # because each alone has look-alikes. Mirrors Test-IsCloudPc in the bootstrapper. $cloudPcRegMarker = Test-RegistryKeySafe -SubKey 'SOFTWARE\Microsoft\Windows365' $cloudPcSvcMarker = $null -ne (Get-Service -Name 'CloudManagedDesktopExtension' -ErrorAction SilentlyContinue) $isCloudPc = ($cloudPcRegMarker -and $cloudPcSvcMarker) $AgentExePath = Join-Path $AgentBinPath 'AutopilotMonitor.Agent.exe' $MaxBootstrapWindowHours = 12 $wouldInstall = $true $reasons = New-Object System.Collections.Generic.List[string] Write-Host '' Write-Host '=== Autopilot Monitor Bootstrap Guard Test (Dry Run) ===' Write-Host '' # Guard 1: Agent was already deployed on this device (survives self-destruct) $deployed = Get-RegistryValueSafe -SubKey 'SOFTWARE\AutopilotMonitor' -Name 'Deployed' if ($deployed) { Write-Step -Status 'SKIP' -Message "Guard 1: Agent was previously deployed at '$deployed'." $wouldInstall = $false $reasons.Add("Previously deployed marker exists: $deployed") } else { Write-Step -Status 'PASS' -Message 'Guard 1: No previous deployment marker found.' } # Collect real (non-special) user profiles as full paths (combined WMI/filesystem view). $excludePattern = '^(defaultuser\d*|Public|Default( User)?|All Users|WDAGUtilityAccount)$' $wmiProfileQueryFailed = $false # NOTE: the outer @() is required. Without it the trailing Where/Select pipeline # unwraps a single result back to a scalar string, so $profilePaths[0] would index # into the string and return its first char ('C') instead of the full path. $profilePaths = @( @( try { Get-CimInstance Win32_UserProfile -ErrorAction Stop | Where-Object { -not $_.Special -and $_.LocalPath -like 'C:\Users\*' } | ForEach-Object { $_.LocalPath } } catch { $wmiProfileQueryFailed = $true } (Get-ChildItem 'C:\Users' -Directory -ErrorAction SilentlyContinue).FullName ) | Where-Object { $_ -and (Split-Path $_ -Leaf) -notmatch $excludePattern } | Select-Object -Unique ) if ($wmiProfileQueryFailed) { Write-Step -Status 'WARN' -Message 'Profiles: WMI query failed, filesystem check still applied.' } $profileNames = $profilePaths | ForEach-Object { Split-Path $_ -Leaf } # Relax: the ONLY exemption to guards 2+3. Two triggers -- OOBE InProgress (Windows # Backup for Organizations restore) or Cloud PC identity (headless provisioning, # profile at first connect). Either additionally requires exactly one fresh profile. $oobeInProgress = Test-OobeInProgress $profileAgeMin = $null $oobeRelax = $false if ($profilePaths.Count -eq 1) { try { $created = (Get-Item -LiteralPath $profilePaths[0] -Force -ErrorAction Stop).CreationTimeUtc $profileAgeMin = [math]::Round(((Get-Date).ToUniversalTime() - $created).TotalMinutes, 1) } catch { } if (($oobeInProgress -or $isCloudPc) -and $null -ne $profileAgeMin -and $profileAgeMin -ge 0 -and $profileAgeMin -lt $OobeProfileMaxAgeMinutes) { $oobeRelax = $true } } Write-Step -Status 'INFO' -Message "Cloud PC markers: Windows365 registry key = $cloudPcRegMarker ; CloudManagedDesktopExtension service = $cloudPcSvcMarker ; IsCloudPc = $isCloudPc" Write-Step -Status 'INFO' -Message "OOBE: OutOfBoxExperienceState InProgress = $oobeInProgress ; profileCount = $($profilePaths.Count) ; singleProfileAgeMin = $profileAgeMin ; relax = $oobeRelax" # Guard 2: No real user profile should exist yet (unless OOBE-relax holds) if ($profilePaths.Count -gt 0 -and -not $oobeRelax) { $names = ($profileNames | Select-Object -First 3) -join ', ' Write-Step -Status 'SKIP' -Message "Guard 2: Real user profile(s) found ($names). Device appears productive." $wouldInstall = $false $reasons.Add("Real user profile(s) found: $names") } elseif ($profilePaths.Count -gt 0 -and $oobeRelax) { $names = ($profileNames | Select-Object -First 3) -join ', ' $trigger = if ($oobeInProgress) { 'OOBE InProgress' } else { 'Cloud PC' } Write-Step -Status 'PASS' -Message "Guard 2: Profile ($names) present but relax active ($trigger + single fresh profile) -- not blocking." } else { Write-Step -Status 'PASS' -Message 'Guard 2: No real user profiles found (combined WMI/filesystem view).' } # Guard 3: LastLoggedOnUser (unless OOBE-relax holds) $lastLoggedOnUser = Get-RegistryValueSafe -SubKey 'SOFTWARE\Microsoft\Windows\CurrentVersion\Authentication\LogonUI' -Name 'LastLoggedOnUser' if ($lastLoggedOnUser -and $lastLoggedOnUser -notmatch 'defaultuser\d*' -and -not $oobeRelax) { Write-Step -Status 'SKIP' -Message "Guard 3: LastLoggedOnUser found ($lastLoggedOnUser). Device appears productive." $wouldInstall = $false $reasons.Add("LastLoggedOnUser found: $lastLoggedOnUser") } elseif ($lastLoggedOnUser -and $lastLoggedOnUser -notmatch 'defaultuser\d*' -and $oobeRelax) { Write-Step -Status 'PASS' -Message "Guard 3: LastLoggedOnUser ($lastLoggedOnUser) present but relax active -- not blocking." } else { Write-Step -Status 'PASS' -Message "Guard 3: No real LastLoggedOnUser (value: '$lastLoggedOnUser')." } # Guard 4: Bootstrap window check (sleep/standby does NOT reset uptime, only real boot does) $lastBoot = $null $uptimeHours = $null try { $lastBoot = (Get-CimInstance Win32_OperatingSystem -ErrorAction Stop).LastBootUpTime $uptimeHours = ((Get-Date) - $lastBoot).TotalHours if ($uptimeHours -gt $MaxBootstrapWindowHours) { if ($oobeRelax -and $isCloudPc) { # Cloud PCs run headless for days before the user's first connect; the # fresh-profile window is the time anchor there, not boot uptime. Write-Step -Status 'PASS' -Message "Guard 4: Device uptime is $([int]$uptimeHours)h (over ${MaxBootstrapWindowHours}h), but the Cloud PC relax is active -- not blocking." } else { Write-Step -Status 'SKIP' -Message "Guard 4: Device uptime is $([int]$uptimeHours)h. Older than accepted bootstrap window of ${MaxBootstrapWindowHours}h." $wouldInstall = $false $reasons.Add("Uptime exceeds bootstrap window: $([int]$uptimeHours)h > ${MaxBootstrapWindowHours}h") } } else { Write-Step -Status 'PASS' -Message "Guard 4: Device uptime is $([int]$uptimeHours)h and within bootstrap window of ${MaxBootstrapWindowHours}h." } } catch { Write-Step -Status 'WARN' -Message 'Guard 4: Could not determine LastBootUpTime / uptime.' } # Agent check: an installed agent (its executable or its task; the marker is guard 1) is # left as it is. $agentTask = Get-ScheduledTask -TaskName 'AutopilotMonitor-Agent' -ErrorAction SilentlyContinue if (Test-Path $AgentExePath) { Write-Step -Status 'SKIP' -Message "Agent check: Agent already installed at '$AgentExePath'." $wouldInstall = $false $reasons.Add("Agent binary already present: $AgentExePath") } elseif ($agentTask) { Write-Step -Status 'SKIP' -Message "Agent check: Agent task 'AutopilotMonitor-Agent' exists." $wouldInstall = $false $reasons.Add("Agent task present: AutopilotMonitor-Agent") } else { Write-Step -Status 'PASS' -Message "Agent check: No agent binary at '$AgentExePath' and no agent task." } Write-Host '' Write-Host '=== Result ===' Write-Host '' if ($wouldInstall) { Write-Host '[DECISION] WOULD INSTALL agent on this device.' } else { Write-Host '[DECISION] WOULD SKIP agent installation on this device.' } Write-Host '' Write-Host '=== Summary ===' if ($reasons.Count -eq 0) { Write-Host 'No blocking reasons found.' } else { foreach ($reason in $reasons) { Write-Host " - $reason" } } Write-Host '' Write-Host '=== Raw Values ===' Write-Host "Deployed marker : $deployed" Write-Host "DetectedProfileNames : $($profileNames -join ', ')" Write-Host "ProfileCount : $($profilePaths.Count)" Write-Host "OobeInProgress (WinRT) : $oobeInProgress" Write-Host "CloudPcRegistryMarker : $cloudPcRegMarker" Write-Host "CloudPcServiceMarker : $cloudPcSvcMarker" Write-Host "IsCloudPc : $isCloudPc" Write-Host "SingleProfileAgeMinutes : $profileAgeMin" Write-Host "OobeProfileMaxAgeMinutes : $OobeProfileMaxAgeMinutes" Write-Host "RelaxActive : $oobeRelax" Write-Host "LastLoggedOnUser : $lastLoggedOnUser" Write-Host "MaxBootstrapWindowHours : $MaxBootstrapWindowHours" Write-Host "LastBootUpTime : $lastBoot" Write-Host "UptimeHours : $([string]$uptimeHours)" Write-Host "AgentExePath : $AgentExePath" Write-Host ''