# w11-snow-setup stage-0. Served by the Worker with the values below filled in. # Contains no secrets. Run with: irm https://setup.example.com | iex # (/audit reports only; /dryrun reports what a full run would do; neither changes the PC.) # Execution policy does not apply here (iex runs text, not a file); everything this # starts from disk is launched with -ExecutionPolicy Bypass for that process only. function Invoke-SnowSetupStage0 { $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' $BaseUrl = 'https://w11setup.snownative.com' $Mode = 'Full' $InstallRoot = 'C:\ame' $ReleaseRef = 'v0.3.0' $EntryUrl = switch ($Mode) { 'Audit' { "$BaseUrl/audit" } 'DryRun' { "$BaseUrl/dryrun" } default { "$BaseUrl/" } } function Write-Stop([string]$Message) { Write-Host '' Write-Host "SETUP STOPPED: $Message" -ForegroundColor Red Write-Host '' } function Get-HttpStatus($ErrorRecord) { try { return [int]$ErrorRecord.Exception.Response.StatusCode } catch { return 0 } } try { [Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 } catch { Write-Verbose 'TLS setting unchanged' } Write-Host "w11-snow-setup ($Mode mode, release $ReleaseRef)" -ForegroundColor Cyan # 1. Elevate. iex has no script file to relaunch, so the elevated window downloads this again. $principal = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent() if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host 'Asking for administrator rights (approve the UAC prompt)...' try { Start-Process -FilePath 'powershell.exe' -Verb RunAs -ArgumentList @( '-NoProfile', '-ExecutionPolicy', 'Bypass', '-NoExit', '-Command', "irm '$EntryUrl' | iex") } catch { Write-Stop 'Administrator rights were declined. Run the command again and approve the UAC prompt.' } return } # 2. Checks that would make the rest fail, done before asking for the PIN. if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage') { Write-Stop "PowerShell is in $($ExecutionContext.SessionState.LanguageMode) mode (AppLocker, WDAC or Smart App Control). Setup needs FullLanguage." return } $cv = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' $build = [int]$cv.CurrentBuildNumber if ($build -lt 22631) { Write-Stop "Windows build $build is too old. Setup needs Windows 11 23H2 (22631) or newer." return } $editions = 'Professional', 'ProfessionalN', 'ProfessionalWorkstation', 'ProfessionalEducation', 'Enterprise', 'EnterpriseN', 'Education', 'EducationN' if ($editions -notcontains $cv.EditionID) { Write-Stop "Edition $($cv.EditionID) is not supported. Setup needs Windows 11 Pro, Enterprise or Education." return } foreach ($scope in 'MachinePolicy', 'UserPolicy') { $policy = Get-ExecutionPolicy -Scope $scope if ($policy -eq 'AllSigned' -or $policy -eq 'Restricted') { Write-Stop "Group Policy sets the execution policy to $policy ($scope), which overrides -ExecutionPolicy Bypass. Run setup before the domain join, or sign the scripts." return } } # 3. Install folder, locked to Administrators and SYSTEM (a folder in the root of C: # otherwise lets any signed-in user change files that later run as admin). New-Item -ItemType Directory -Path (Join-Path $InstallRoot 'Logs') -Force | Out-Null & icacls.exe $InstallRoot /inheritance:r /grant:r '*S-1-5-32-544:(OI)(CI)F' '*S-1-5-18:(OI)(CI)F' | Out-Null if ($LASTEXITCODE -ne 0) { Write-Stop "Could not set permissions on $InstallRoot (icacls exit $LASTEXITCODE)." return } # 4. PIN -> session token. $auth = $null for ($try = 1; $try -le 3 -and -not $auth; $try++) { $secure = Read-Host 'Setup PIN' -AsSecureString $bstr = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($secure) try { $pin = [Runtime.InteropServices.Marshal]::PtrToStringBSTR($bstr) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) } try { $auth = Invoke-RestMethod -Method Post -Uri "$BaseUrl/auth" -ContentType 'application/json' -Body (@{ pin = $pin } | ConvertTo-Json) -UseBasicParsing } catch { $status = Get-HttpStatus $_ if ($status -eq 401) { Write-Host "Wrong PIN ($try of 3)." -ForegroundColor Yellow } elseif ($status -eq 429) { Write-Stop 'Too many PIN attempts. Wait a minute, then run the command again.'; return } elseif ($status -eq 0) { Write-Stop "Cannot reach $BaseUrl. Check the network connection. ($($_.Exception.Message))"; return } else { Write-Stop "The setup server answered HTTP $status."; return } } finally { $pin = $null } } if (-not $auth) { Write-Stop 'Wrong PIN three times.'; return } # 5. Payload: download, verify, extract. $zip = Join-Path $InstallRoot 'payload.zip' Write-Host "Downloading release $($auth.release)..." try { $resp = Invoke-WebRequest -Uri "$BaseUrl/bundle" -Headers @{ Authorization = "Bearer $($auth.session)" } -OutFile $zip -PassThru -UseBasicParsing } catch { Write-Stop "Download failed (HTTP $(Get-HttpStatus $_)). $($_.Exception.Message)" return } $expected = [string]$resp.Headers['X-Payload-Sha256'] $actual = (Get-FileHash -Path $zip -Algorithm SHA256).Hash if (-not $expected -or $actual -ne $expected) { Write-Stop "Payload checksum mismatch (expected '$expected', got '$actual')." return } Expand-Archive -Path $zip -DestinationPath $InstallRoot -Force Get-ChildItem -Path $InstallRoot -Recurse -File | Unblock-File # 6. State for Setup.ps1. The folder is admin-only, so the tokens are not readable by users. $statePath = Join-Path $InstallRoot 'state.json' $state = $null if (Test-Path $statePath) { try { $state = Get-Content $statePath -Raw | ConvertFrom-Json } catch { $state = $null } } if (-not $state) { $state = [pscustomobject]@{ mode = $Mode createdUtc = (Get-Date).ToUniversalTime().ToString('o') bootstrapUserSid = ([Security.Principal.WindowsIdentity]::GetCurrent()).User.Value rebootCount = 0 steps = [pscustomobject]@{} } } $state | Add-Member -NotePropertyName mode -NotePropertyValue $Mode -Force $state | Add-Member -NotePropertyName baseUrl -NotePropertyValue $BaseUrl -Force $state | Add-Member -NotePropertyName releaseRef -NotePropertyValue ([string]$auth.release) -Force $state | Add-Member -NotePropertyName session -NotePropertyValue ([string]$auth.session) -Force $state | Add-Member -NotePropertyName sessionExpires -NotePropertyValue ([long]$auth.sessionExpires) -Force $state | Add-Member -NotePropertyName reportToken -NotePropertyValue ([string]$auth.reportToken) -Force $state | Add-Member -NotePropertyName joinToken -NotePropertyValue ([string]$auth.joinToken) -Force $state | ConvertTo-Json -Depth 10 | Set-Content -Path $statePath -Encoding UTF8 # 7. Hand over to the engine in its own window. $setup = Join-Path $InstallRoot 'Setup.ps1' Start-Process -FilePath 'powershell.exe' -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', "`"$setup`"", '-Mode', $Mode) Write-Host "Setup started in a new window. Logs: $InstallRoot\Logs" -ForegroundColor Green } Invoke-SnowSetupStage0