diff --git a/data/os/Windows.yaml b/data/os/Windows.yaml index 240339c60..e94b030bc 100644 --- a/data/os/Windows.yaml +++ b/data/os/Windows.yaml @@ -55,7 +55,9 @@ windows: relops_az: "https://roninpuppetassets.blob.core.windows.net/binaries/taskcluster" relops_s3: "https://s3-us-west-2.amazonaws.com/ronin-puppet-package-repo/Windows/taskcluster" download_url: "https://github.com/taskcluster/taskcluster/releases/download" - version: "93.1.1" + version: "93.1.4" + hw_version: "91.1.0" + generic-worker: name: amd64: "generic-worker-multiuser-windows-amd64" diff --git a/data/roles/win116424h2hw.yaml b/data/roles/win116424h2hw.yaml index 7e7ef0f83..efe65b488 100644 --- a/data/roles/win116424h2hw.yaml +++ b/data/roles/win116424h2hw.yaml @@ -18,5 +18,3 @@ win-worker: ## Use to overwrite values in the Windows yaml file. ## Replace windows. with win-worker.variant. variant: - taskcluster: - version: "91.1.0" diff --git a/data/roles/win116424h2hwalpha.yaml b/data/roles/win116424h2hwalpha.yaml index 62e6718af..717a61798 100644 --- a/data/roles/win116424h2hwalpha.yaml +++ b/data/roles/win116424h2hwalpha.yaml @@ -18,5 +18,3 @@ win-worker: ## Use to overwrite values in the Windows yaml file. ## Replace windows. with win-worker.variant. variant: - taskcluster: - version: "91.1.0" diff --git a/data/roles/win116424h2hwperfsheriff.yaml b/data/roles/win116424h2hwperfsheriff.yaml index 2d4d94c34..eeffd0093 100644 --- a/data/roles/win116424h2hwperfsheriff.yaml +++ b/data/roles/win116424h2hwperfsheriff.yaml @@ -19,5 +19,3 @@ win-worker: ## Use to overwrite values in the Windows yaml file. ## Replace windows. with win-worker.variant. variant: - taskcluster: - version: "91.1.0" diff --git a/data/roles/win116424h2hwref.yaml b/data/roles/win116424h2hwref.yaml index 917cf305e..7393de1b7 100644 --- a/data/roles/win116424h2hwref.yaml +++ b/data/roles/win116424h2hwref.yaml @@ -18,5 +18,3 @@ win-worker: ## Use to overwrite values in the Windows yaml file. ## Replace windows. with win-worker.variant. variant: - taskcluster: - version: "91.1.0" diff --git a/data/roles/win116424h2hwrefalpha.yaml b/data/roles/win116424h2hwrefalpha.yaml index 7551367e6..5c0c99905 100644 --- a/data/roles/win116424h2hwrefalpha.yaml +++ b/data/roles/win116424h2hwrefalpha.yaml @@ -18,5 +18,3 @@ win-worker: ## Use to overwrite values in the Windows yaml file. ## Replace windows. with win-worker.variant. variant: - taskcluster: - version: "91.1.0" diff --git a/data/roles/win116424h2hwrelops1213.yaml b/data/roles/win116424h2hwrelops1213.yaml index a0ca52cf8..4ea5ee74c 100644 --- a/data/roles/win116424h2hwrelops1213.yaml +++ b/data/roles/win116424h2hwrelops1213.yaml @@ -19,5 +19,3 @@ win-worker: ## Use to overwrite values in the Windows yaml file. ## Replace windows. with win-worker.variant. variant: - taskcluster: - version: "91.1.0" diff --git a/modules/roles_profiles/manifests/profiles/disable_services.pp b/modules/roles_profiles/manifests/profiles/disable_services.pp index 5d4ce9175..1617def02 100644 --- a/modules/roles_profiles/manifests/profiles/disable_services.pp +++ b/modules/roles_profiles/manifests/profiles/disable_services.pp @@ -39,17 +39,36 @@ include win_disable_services::disable_windows_update if $facts['custom_win_purpose'] != builder { include win_disable_services::disable_wsearch - ## Let's Uninstall Appx Packages - ## Taken from https://github.com/The-Virtual-Desktop-Team/Virtual-Desktop-Optimization-Tool - ## Bug 1913499 https://bugzilla.mozilla.org/show_bug.cgi?id=1913499 - include win_disable_services::uninstall_appx_packages - if ($facts['custom_win_location'] == 'azure') { - include win_scheduled_tasks::kill_local_clipboard - } + ## WIP for RELOPS-1946 + ## Not currently working. Leaving n place for ref. + #include win_disable_services::disable_defender_smartscreen + #include win_disable_services::disable_sync_from_cloud if $facts['custom_win_release_id'] == '2004' or '2009' { ## win11 ref with osdcloud include win_disable_services::disable_windows_defender_schtask } + case $facts['custom_win_location'] { + 'datacenter': { + $apx_uninstall = 'hw-uninstall.ps1' + include win_disable_services::disable_optional_services + } + 'azure': { + include win_scheduled_tasks::kill_local_clipboard + $apx_uninstall = 'uninstall.ps1' + } + default: { + } + } + ## Let's Uninstall Appx Packages + ## Taken from https://github.com/The-Virtual-Desktop-Team/Virtual-Desktop-Optimization-Tool + ## Bug 1913499 https://bugzilla.mozilla.org/show_bug.cgi?id=1913499 + class { 'win_disable_services::uninstall_appx_packages': + apx_uninstall => $apx_uninstall + } + ## must be ran after apx uninstall + if ($facts['custom_win_location'] == 'datacenter') { + include win_disable_services::disable_ms_edge + } } # May be needed for non-hardaware # Commented out because this will break the auto restore diff --git a/modules/roles_profiles/manifests/profiles/scheduled_tasks.pp b/modules/roles_profiles/manifests/profiles/scheduled_tasks.pp index 7fcb53c8e..a4db1b25a 100644 --- a/modules/roles_profiles/manifests/profiles/scheduled_tasks.pp +++ b/modules/roles_profiles/manifests/profiles/scheduled_tasks.pp @@ -11,8 +11,9 @@ $startup_script = 'azure-maintainsystem.ps1' } 'datacenter': { - #$startup_script = 'maintainsystem-hw.ps1' - $startup_script = 'maintainsystem-reftester.ps1' + $startup_script = 'maintainsystem-hw.ps1' + include win_scheduled_tasks::self_redeploy_check + include win_scheduled_tasks::gw_exe_check } default: { $startup_script = 'maintainsystem.ps1' diff --git a/modules/roles_profiles/manifests/profiles/windows_generic_worker_standalone.pp b/modules/roles_profiles/manifests/profiles/windows_generic_worker_standalone.pp index 0b58360a8..5c170780c 100644 --- a/modules/roles_profiles/manifests/profiles/windows_generic_worker_standalone.pp +++ b/modules/roles_profiles/manifests/profiles/windows_generic_worker_standalone.pp @@ -9,7 +9,7 @@ $arch = 'win64' - $ext_pkg_src_loc = lookup('windows.taskcluster.relops_az') + $ext_pkg_src_loc = "${lookup('windows.taskcluster.relops_az')}/" $generic_worker_dir = lookup('windows.dir.generic_worker') $gw_exe_path = "${generic_worker_dir}\\generic-worker.exe" @@ -51,14 +51,14 @@ generic_worker_dir => $generic_worker_dir, gw_config_path => $gw_config_path, gw_exe_path => $gw_exe_path, - gw_exe_source => "${ext_pkg_src_loc}/${taskcluster_version}/${gw_name}", + gw_exe_source => "${ext_pkg_src_loc}${taskcluster_version}/${gw_name}", gw_status => $facts['custom_win_genericworker_service'], livelog_exe => "${facts['custom_win_systemdrive']}\\\\generic-worker\\\\livelog.exe", - livelog_exe_source => "${ext_pkg_src_loc}/${$taskcluster_version}/${livelog_name}", + livelog_exe_source => "${ext_pkg_src_loc}${taskcluster_version}/${livelog_name}", task_dir => "${facts['custom_win_systemdrive']}\\\\", taskcluster_access_token => lookup('taskcluster_access_token'), taskcluster_proxy_exe => "${facts['custom_win_systemdrive']}\\\\generic-worker\\\\taskcluster-proxy.exe", - taskcluster_proxy_source => "${ext_pkg_src_loc}/${taskcluster_version}/${proxy_name}", + taskcluster_proxy_source => "${ext_pkg_src_loc}${taskcluster_version}/${proxy_name}", taskcluster_root => lookup('windows.taskcluster.root_url'), #task_user_init_cmd => $init, worker_type => $worker_pool_id, diff --git a/modules/roles_profiles/manifests/profiles/windows_worker_runner.pp b/modules/roles_profiles/manifests/profiles/windows_worker_runner.pp index 08ffb3f94..2284a628b 100644 --- a/modules/roles_profiles/manifests/profiles/windows_worker_runner.pp +++ b/modules/roles_profiles/manifests/profiles/windows_worker_runner.pp @@ -11,20 +11,18 @@ case $facts['custom_win_location'] { 'datacenter': { - $ext_pkg_src_loc = lookup('windows.taskcluster.relops_s3') + $ext_pkg_src_loc = "${lookup('windows.taskcluster.relops_az')}/" $provider = 'standalone' + $taskcluster_version = + lookup(['win-worker.variant.taskcluster.version', 'windows.taskcluster.hw_version']) } default: { - #$ext_pkg_src_loc = lookup('windows.taskcluster.relops_az') $ext_pkg_src_loc = "${lookup('windows.taskcluster.download_url')}/v" - #$ext_pkg_src_loc = "https://github.com/taskcluster/taskcluster/releases/download/v93.1.4/generic-worker-multiuser-windows-arm64" $provider = lookup('windows.taskcluster.worker_runner.provider') + $taskcluster_version = + lookup(['win-worker.variant.taskcluster.version', 'windows.taskcluster.version']) } } - - $taskcluster_version = - lookup(['win-worker.variant.taskcluster.version', 'windows.taskcluster.version']) - case $facts['custom_win_os_arch'] { 'aarch64': { $gw_name = lookup('windows.taskcluster.generic-worker.name.arm64') diff --git a/modules/roles_profiles/manifests/roles/win116424h2hwrelops1213.pp b/modules/roles_profiles/manifests/roles/win116424h2hwrelops1213.pp index c4c34b649..9e876d4a8 100644 --- a/modules/roles_profiles/manifests/roles/win116424h2hwrelops1213.pp +++ b/modules/roles_profiles/manifests/roles/win116424h2hwrelops1213.pp @@ -21,6 +21,7 @@ # Adminstration include roles_profiles::profiles::logging + include roles_profiles::profiles::mercurial include roles_profiles::profiles::nuc_management #include roles_profiles::profiles::vnc @@ -28,7 +29,8 @@ include roles_profiles::profiles::git include roles_profiles::profiles::mozilla_build include roles_profiles::profiles::mozilla_maintenance_service - include roles_profiles::profiles::windows_generic_worker_standalone include roles_profiles::profiles::windows_datacenter_administrator include roles_profiles::profiles::google_chrome + #include roles_profiles::profiles::windows_generic_worker_standalone + include roles_profiles::profiles::windows_worker_runner } diff --git a/modules/win_disable_services/files/appxpackages/hw-uninstall.ps1 b/modules/win_disable_services/files/appxpackages/hw-uninstall.ps1 new file mode 100644 index 000000000..42bd7dcd1 --- /dev/null +++ b/modules/win_disable_services/files/appxpackages/hw-uninstall.ps1 @@ -0,0 +1,334 @@ +function Write-Log { + param ( + [string] $message, + [ValidateSet('DEBUG','INFO','WARN','ERROR')] + [string] $severity = 'INFO', + [string] $source = 'BootStrap', + [string] $logName = 'Application' + ) + + $entryType = 'Information' + $eventId = 1 + + switch ($severity) { + 'DEBUG' { $entryType = 'SuccessAudit'; $eventId = 2; break } + 'WARN' { $entryType = 'Warning'; $eventId = 3; break } + 'ERROR' { $entryType = 'Error'; $eventId = 4; break } + default { $entryType = 'Information'; $eventId = 1; break } + } + + # Best-effort event log creation (avoid terminating failures / races) + try { + if (!([Diagnostics.EventLog]::Exists($logName)) -or + !([Diagnostics.EventLog]::SourceExists($source))) { + New-EventLog -LogName $logName -Source $source -ErrorAction SilentlyContinue | Out-Null + } + } catch { + # ignore + } + + try { + Write-EventLog -LogName $logName -Source $source ` + -EntryType $entryType -Category 0 -EventID $eventId ` + -Message $message -ErrorAction SilentlyContinue + } catch { + # ignore + } + + if ([Environment]::UserInteractive) { + $fc = @{ + 'Information' = 'White' + 'Error' = 'Red' + 'Warning' = 'DarkYellow' + 'SuccessAudit' = 'DarkGray' + }[$entryType] + Write-Host $message -ForegroundColor $fc + } +} + +# IMPORTANT: use 'Continue' so normal AppX noise doesn't hard-fail Puppet +$ErrorActionPreference = 'Continue' + +$svcName = 'AppXSvc' +$svcKeyPath = 'HKLM:\SYSTEM\CurrentControlSet\Services\AppXSvc' + +function Remove-PreinstalledAppxPackages { + [CmdletBinding()] + param() + + $apps = @{ + "Bing Search" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9nzbf4gt040c"; Description="Web Search from Microsoft Bing provides web results and answers in Windows Search" } + "Clipchamp.Clipchamp" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9p1j8s7ccwwt?hl=en-us&gl=US"; Description="Create videos with a few clicks" } + "Microsoft.549981C3F5F10" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/cortana/9NFFX4SZZ23L?hl=en-us&gl=US"; Description="Cortana (could not update)" } + "Microsoft.BingNews" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-news/9wzdncrfhvfw"; Description="Microsoft News app" } + "Microsoft.BingWeather" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/msn-weather/9wzdncrfj3q2"; Description="MSN Weather app" } + ## Doesn't actually gets removed + ## Comment out for now + #"Microsoft.DesktopAppInstaller" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9NBLGGH4NNS1"; Description="Microsoft App Installer for Windows 10 makes sideloading Windows apps easy" } + "Microsoft.GetHelp" = @{ VDIState="Unchanged"; URL="https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/customize-get-help-app"; Description="App that facilitates free support for Microsoft products" } + "Microsoft.Getstarted" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-tips/9wzdncrdtbjj"; Description="Windows 10 tips app" } + "Microsoft.MicrosoftOfficeHub" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/office/9wzdncrd29v9"; Description="Office UWP app suite" } + "Microsoft.Office.OneNote" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/onenote-for-windows-10/9wzdncrfhvjl"; Description="Office UWP OneNote app" } + "Microsoft.MicrosoftSolitaireCollection" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-solitaire-collection/9wzdncrfhwd2"; Description="Solitaire suite of games" } + "Microsoft.MicrosoftStickyNotes" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-sticky-notes/9nblggh4qghw"; Description="Note-taking app" } + "Microsoft.OutlookForWindows" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9NRX63209R7B?hl=en-us&gl=US"; Description="New Outlook app" } + "Microsoft.MSPaint" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/store/detail/paint-3d/9NBLGGH5FV99"; Description="Paint 3D app" } + "Microsoft.Paint" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9PCFS5B6T72H"; Description="Classic Paint app" } + "Microsoft.People" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-people/9nblggh10pg8"; Description="Contact management app" } + "Microsoft.PowerAutomateDesktop" = @{ VDIState="Unchanged"; URL="https://flow.microsoft.com/en-us/desktop/"; Description="Power Automate Desktop" } + "Microsoft.ScreenSketch" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/snip-sketch/9mz95kl8mr0l"; Description="Snip and Sketch app" } + "Microsoft.SkypeApp" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/skype/9wzdncrfj364"; Description="Skype app" } + "Microsoft.StorePurchaseApp" = @{ VDIState="Unchanged"; URL=""; Description="Store purchase app helper" } + "Microsoft.Todos" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-to-do-lists-tasks-reminders/9nblggh5r558"; Description="Microsoft To Do" } + "Microsoft.WinDbg.Fast" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9PGJGD53TN86?hl=en-us&gl=US"; Description="WinDbg" } + "Microsoft.Windows.DevHome" = @{ VDIState="Unchanged"; URL="https://learn.microsoft.com/en-us/windows/dev-home/"; Description="Dev Home dashboard" } + "Microsoft.Windows.Photos" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/microsoft-photos/9wzdncrfjbh4"; Description="Photos app" } + "Microsoft.WindowsAlarms" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-alarms-clock/9wzdncrfj3pr"; Description="Alarms & Clock" } + "Microsoft.WindowsCalculator" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-calculator/9wzdncrfhvn5"; Description="Calculator" } + "Microsoft.WindowsCamera" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-camera/9wzdncrfjbbg"; Description="Camera" } + "microsoft.windowscommunicationsapps" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/mail-and-calendar/9wzdncrfhvqm"; Description="Mail & Calendar" } + "Microsoft.WindowsFeedbackHub" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/feedback-hub/9nblggh4r32n"; Description="Feedback Hub" } + "Microsoft.WindowsMaps" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-maps/9wzdncrdtbvb"; Description="Maps" } + "Microsoft.WindowsNotepad" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-notepad/9msmlrh6lzf3"; Description="Notepad (Store)" } + "Microsoft.WindowsStore" = @{ VDIState="Unchanged"; URL="https://blogs.windows.com/windowsexperience/2021/06/24/building-a-new-open-microsoft-store-on-windows-11/"; Description="Microsoft Store" } + "Microsoft.WindowsSoundRecorder" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-voice-recorder/9wzdncrfhwkn"; Description="Voice Recorder" } + "Microsoft.WindowsTerminal" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701"; Description="Windows Terminal" } + "Microsoft.Winget.Platform.Source"= @{ VDIState="Unchanged"; URL="https://learn.microsoft.com/en-us/windows/package-manager/winget/"; Description="Winget source" } + "Microsoft.Xbox.TCUI" = @{ VDIState="Unchanged"; URL="https://docs.microsoft.com/en-us/gaming/xbox-live/features/general/tcui/live-tcui-overview"; Description="Xbox TCUI" } + "Microsoft.XboxIdentityProvider" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/xbox-identity-provider/9wzdncrd1hkw"; Description="Xbox Identity Provider" } + "Microsoft.XboxSpeechToTextOverlay" = @{ VDIState="Unchanged"; URL="https://support.xbox.com/help/account-profile/accessibility/use-game-chat-transcription"; Description="Xbox chat transcription" } + "Microsoft.YourPhone" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/Your-phone/9nmpj99vjbwv"; Description="Phone Link" } + "Microsoft.ZuneMusic" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/groove-music/9wzdncrfj3pt"; Description="Groove Music" } + "Microsoft.ZuneVideo" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/movies-tv/9wzdncrfj3p2"; Description="Movies & TV" } + "MicrosoftCorporationII.QuickAssist" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/9P7BP5VNWKX5?hl=en-us&gl=US"; Description="Quick Assist" } + "MicrosoftWindows.Client.WebExperience" = @{ VDIState="Unchanged"; URL=""; Description="Windows 11 Web Experience" } + "Microsoft.XboxApp" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/store/apps/9wzdncrfjbd8"; Description="Xbox Console Companion" } + "Microsoft.MixedReality.Portal" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/mixed-reality-portal/9ng1h8b3zc7m"; Description="Mixed Reality Portal" } + "Microsoft.Microsoft3DViewer" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/p/3d-viewer/9nblggh42ths"; Description="3D Viewer" } + "MicrosoftTeams" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/xp8bt8dw290mpq"; Description="Microsoft Teams" } + "MSTeams" = @{ VDIState="Unchanged"; URL="https://apps.microsoft.com/detail/xp8bt8dw290mpq"; Description="Microsoft Teams (alt id)" } + "Microsoft.OneDriveSync" = @{ VDIState="Unchanged"; URL="https://docs.microsoft.com/en-us/onedrive/one-drive-sync"; Description="OneDrive sync app" } + "Microsoft.Wallet" = @{ VDIState="Unchanged"; URL="https://www.microsoft.com/en-us/payments"; Description="Microsoft Pay" } + } + + foreach ($Key in $apps.Keys) { + try { + Write-Log -message ("uninstall_appx_packages :: removing AppX match: {0}" -f $Key) -severity 'DEBUG' + + # Provisioned packages (image-level) + try { + Get-AppxProvisionedPackage -Online -ErrorAction Stop | + Where-Object { $_.PackageName -like ("*{0}*" -f $Key) } | + ForEach-Object { + $pkgName = $_.PackageName + try { + Remove-AppxProvisionedPackage -Online -PackageName $pkgName -ErrorAction Stop | Out-Null + } catch { + Write-Log -message ("Remove-AppxProvisionedPackage failed for {0}: {1}" -f $pkgName, $_.Exception.Message) -severity 'WARN' + } + } + } catch { + Write-Log -message ("Get/Remove provisioned package failed for key {0}: {1}" -f $Key, $_.Exception.Message) -severity 'WARN' + } + + # Installed packages (all users) + try { + Get-AppxPackage -AllUsers -Name ("*{0}*" -f $Key) -ErrorAction SilentlyContinue | + ForEach-Object { + $full = $_.PackageFullName + try { + Remove-AppxPackage -AllUsers -Package $full -ErrorAction Stop | Out-Null + } catch { + Write-Log -message ("Remove-AppxPackage(-AllUsers) failed for {0}: {1}" -f $full, $_.Exception.Message) -severity 'WARN' + } + } + } catch { + Write-Log -message ("Get/Remove AppxPackage(-AllUsers) failed for key {0}: {1}" -f $Key, $_.Exception.Message) -severity 'WARN' + } + + # Installed packages (current user) + try { + Get-AppxPackage -Name ("*{0}*" -f $Key) -ErrorAction SilentlyContinue | + ForEach-Object { + $full = $_.PackageFullName + try { + Remove-AppxPackage -Package $full -ErrorAction Stop | Out-Null + } catch { + Write-Log -message ("Remove-AppxPackage failed for {0}: {1}" -f $full, $_.Exception.Message) -severity 'WARN' + } + } + } catch { + Write-Log -message ("Get/Remove AppxPackage failed for key {0}: {1}" -f $Key, $_.Exception.Message) -severity 'WARN' + } + } catch { + # Absolutely never let AppX errors terminate this script (Puppet signal should be AppXSvc-only) + Write-Log -message ("Remove-PreinstalledAppxPackages unexpected failure for key {0}: {1}" -f $Key, $_.Exception.ToString()) -severity 'WARN' + continue + } + } +} + +function Disable-AppXSvcCore { + [CmdletBinding()] + param() + + $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue + if ($null -ne $svc) { + if ($svc.Status -ne 'Stopped') { + Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue + } + Set-Service -Name $svcName -StartupType Disabled -ErrorAction SilentlyContinue + } + + # Extra-hard disable (best-effort): do NOT allow sc.exe exit code to poison overall script exit code + try { + & sc.exe config $svcName start= disabled | Out-Null + } catch { + # ignore + } finally { + $global:LASTEXITCODE = 0 + } + + # Registry is the source of truth for disabled start + if (Test-Path $svcKeyPath) { + New-ItemProperty -Path $svcKeyPath -Name Start -Value 4 -PropertyType DWord -Force | Out-Null + } +} + +function Ensure-AppXSvcHardeningTask { + [CmdletBinding()] + param() + + $hardeningDir = 'C:\ProgramData\AppXLock' + $hardeningFile = Join-Path $hardeningDir 'Disable-AppXSvc.ps1' + + if (-not (Test-Path $hardeningDir)) { + New-Item -ItemType Directory -Path $hardeningDir -Force | Out-Null + } + + $hardeningScript = @' +param() + +$ErrorActionPreference = "SilentlyContinue" + +$svcName = "AppXSvc" +$svcKeyPath = "HKLM:\SYSTEM\CurrentControlSet\Services\AppXSvc" + +try { + $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue + if ($null -ne $svc) { + if ($svc.Status -ne "Stopped") { + Stop-Service -Name $svcName -Force -ErrorAction SilentlyContinue + } + Set-Service -Name $svcName -StartupType Disabled -ErrorAction SilentlyContinue + } + + # Best-effort: do NOT leak sc.exe exit code + try { + & sc.exe config $svcName start= disabled | Out-Null + } catch { + # ignore + } finally { + $global:LASTEXITCODE = 0 + } + + if (Test-Path $svcKeyPath) { + New-ItemProperty -Path $svcKeyPath -Name Start -Value 4 -PropertyType DWord -Force | Out-Null + } +} catch { + # best-effort only +} +'@ + + Set-Content -Path $hardeningFile -Value $hardeningScript -Encoding UTF8 -Force + + $action = New-ScheduledTaskAction -Execute 'powershell.exe' ` + -Argument "-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -ExecutionPolicy Bypass -File `"$hardeningFile`"" + $trigger = New-ScheduledTaskTrigger -AtStartup + + $taskName = 'Hard-Disable-AppXSvc' + $taskPath = '\Hardening\' + + Unregister-ScheduledTask -TaskName $taskName -TaskPath $taskPath -Confirm:$false -ErrorAction SilentlyContinue + + Register-ScheduledTask -TaskName $taskName ` + -TaskPath $taskPath ` + -Action $action ` + -Trigger $trigger ` + -RunLevel Highest ` + -User 'SYSTEM' ` + -Force | Out-Null +} + +function Test-AppXSvcDisabled { + [CmdletBinding()] + param() + + $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue + if ($null -eq $svc) { return $true } + + # Registry is the most reliable indicator (Start=4) + $regStart = $null + try { + $regStart = (Get-ItemProperty -Path $svcKeyPath -Name Start -ErrorAction SilentlyContinue).Start + } catch { } + + $regDisabled = ($regStart -eq 4) + + # CIM is a helpful second signal (StartMode: Auto/Manual/Disabled) + $cimDisabled = $false + $cimStartMode = 'Unknown' + try { + $svcCim = Get-CimInstance Win32_Service -Filter "Name='$svcName'" -ErrorAction SilentlyContinue + if ($svcCim) { + $cimStartMode = $svcCim.StartMode + $cimDisabled = ($svcCim.StartMode -eq 'Disabled') + } + } catch { } + + if ($svc.Status -eq 'Stopped' -and ($regDisabled -or $cimDisabled)) { + return $true + } + + return $false +} + +# --- Main flow --------------------------------------------------------------- + +try { + Write-Log -message 'uninstall_appx_packages :: begin' -severity 'DEBUG' + + Write-Log -message 'uninstall_appx_packages :: Remove-PreinstalledAppxPackages' -severity 'DEBUG' + Remove-PreinstalledAppxPackages + + Write-Log -message 'uninstall_appx_packages :: Disable-AppXSvcCore' -severity 'DEBUG' + Disable-AppXSvcCore + + Write-Log -message 'uninstall_appx_packages :: Ensure-AppXSvcHardeningTask' -severity 'DEBUG' + Ensure-AppXSvcHardeningTask + + if (-not (Test-AppXSvcDisabled)) { + $svc = Get-Service -Name $svcName -ErrorAction SilentlyContinue + $status = if ($svc) { $svc.Status } else { 'Missing' } + + $regStart = $null + try { $regStart = (Get-ItemProperty -Path $svcKeyPath -Name Start -ErrorAction SilentlyContinue).Start } catch { } + $regStartStr = if ($null -ne $regStart) { $regStart } else { 'Missing' } + + $cimStartMode = 'Unknown' + try { + $svcCim = Get-CimInstance Win32_Service -Filter "Name='$svcName'" -ErrorAction SilentlyContinue + if ($svcCim) { $cimStartMode = $svcCim.StartMode } + } catch { } + + Write-Log -message ("uninstall_appx_packages :: AppXSvc is NOT disabled. Status: {0}, RegStart: {1}, CimStartMode: {2}" -f $status, $regStartStr, $cimStartMode) -severity 'ERROR' + exit 2 + } + + Write-Log -message 'uninstall_appx_packages :: complete (AppXSvc disabled)' -severity 'DEBUG' + exit 0 +} +catch { + Write-Log -message ("uninstall_appx_packages :: FATAL: {0}" -f $_.Exception.ToString()) -severity 'ERROR' + exit 1 +} diff --git a/modules/win_disable_services/files/appxpackages/uninstall.ps1 b/modules/win_disable_services/files/appxpackages/uninstall.ps1 index 8cdbf6742..4778d3e8e 100644 --- a/modules/win_disable_services/files/appxpackages/uninstall.ps1 +++ b/modules/win_disable_services/files/appxpackages/uninstall.ps1 @@ -1,269 +1,320 @@ $apps = @{ - "Bing Search" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9nzbf4gt040c" - "Description" = "Web Search from Microsoft Bing provides web results and answers in Windows Search" - } - "Clipchamp.Clipchamp" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9p1j8s7ccwwt?hl=en-us&gl=US" - "Description" = "Create videos with a few clicks" - } - "Microsoft.549981C3F5F10" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/cortana/9NFFX4SZZ23L?hl=en-us&gl=US" - "Description" = "Cortana (could not update)" - } - "Microsoft.BingNews" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-news/9wzdncrfhvfw" - "Description" = "Microsoft News app" - } - "Microsoft.BingWeather" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/msn-weather/9wzdncrfj3q2" - "Description" = "MSN Weather app" - } - "Microsoft.DesktopAppInstaller" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9NBLGGH4NNS1" - "Description" = "Microsoft App Installer for Windows 10 makes sideloading Windows apps easy" - } - "Microsoft.GetHelp" = @{ - "VDIState" = "Unchanged" - "URL" = "https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/customize-get-help-app" - "Description" = "App that facilitates free support for Microsoft products" - } - "Microsoft.Getstarted" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-tips/9wzdncrdtbjj" - "Description" = "Windows 10 tips app" - } - "Microsoft.MicrosoftOfficeHub" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/office/9wzdncrd29v9" - "Description" = "Office UWP app suite" - } - "Microsoft.Office.OneNote" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/onenote-for-windows-10/9wzdncrfhvjl" - "Description" = "Office UWP OneNote app" + "Bing Search" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9nzbf4gt040c" + Description = "Web Search from Microsoft Bing provides web results and answers in Windows Search" } + + "Clipchamp.Clipchamp" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9p1j8s7ccwwt?hl=en-us&gl=US" + Description = "Create videos with a few clicks" + } + + "Microsoft.549981C3F5F10" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/cortana/9NFFX4SZZ23L?hl=en-us&gl=US" + Description = "Cortana (could not update)" + } + + "Microsoft.BingNews" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-news/9wzdncrfhvfw" + Description = "Microsoft News app" + } + + "Microsoft.BingWeather" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/msn-weather/9wzdncrfj3q2" + Description = "MSN Weather app" + } + + "Microsoft.DesktopAppInstaller" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9NBLGGH4NNS1" + Description = "Microsoft App Installer for Windows 10 makes sideloading Windows apps easy" + } + + "Microsoft.GetHelp" = @{ + VDIState = "Unchanged" + URL = "https://docs.microsoft.com/en-us/windows-hardware/customize/desktop/customize-get-help-app" + Description = "App that facilitates free support for Microsoft products" + } + + "Microsoft.Getstarted" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-tips/9wzdncrdtbjj" + Description = "Windows 10 tips app" + } + + "Microsoft.MicrosoftOfficeHub" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/office/9wzdncrd29v9" + Description = "Office UWP app suite" + } + + "Microsoft.Office.OneNote" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/onenote-for-windows-10/9wzdncrfhvjl" + Description = "Office UWP OneNote app" + } + "Microsoft.MicrosoftSolitaireCollection" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-solitaire-collection/9wzdncrfhwd2" - "Description" = "Solitaire suite of games" - } - "Microsoft.MicrosoftStickyNotes" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-sticky-notes/9nblggh4qghw" - "Description" = "Note-taking app" - } - "Microsoft.OutlookForWindows" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9NRX63209R7B?hl=en-us&gl=US" - "Description" = "a best-in-class email experience that is free for anyone with Windows" - } - "Microsoft.MSPaint" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/store/detail/paint-3d/9NBLGGH5FV99" - "Description" = "Paint 3D app (not Classic Paint app)" - } - "Microsoft.Paint" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9PCFS5B6T72H" - "Description" = "Classic Paint app" - } - "Microsoft.People" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-people/9nblggh10pg8" - "Description" = "Contact management app" - } - "Microsoft.PowerAutomateDesktop" = @{ - "VDIState" = "Unchanged" - "URL" = "https://flow.microsoft.com/en-us/desktop/" - "Description" = "Power Automate Desktop app. Record desktop and web actions in a single flow" - } - "Microsoft.ScreenSketch" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/snip-sketch/9mz95kl8mr0l" - "Description" = "Snip and Sketch app" - } - "Microsoft.SkypeApp" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/skype/9wzdncrfj364" - "Description" = "Instant message, voice or video call app" - } - "Microsoft.StorePurchaseApp" = @{ - "VDIState" = "Unchanged" - "URL" = "" - "Description" = "Store purchase app helper" - } - "Microsoft.Todos" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-to-do-lists-tasks-reminders/9nblggh5r558" - "Description" = "Microsoft To Do makes it easy to plan your day and manage your life" - } - "Microsoft.WinDbg.Fast" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9PGJGD53TN86?hl=en-us&gl=US" - "Description" = "Microsoft WinDbg" - } - "Microsoft.Windows.DevHome" = @{ - "VDIState" = "Unchanged" - "URL" = "https://learn.microsoft.com/en-us/windows/dev-home/" - "Description" = "A control center providing the ability to monitor projects in your dashboard using customizable widgets and more" - } - "Microsoft.Windows.Photos" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/microsoft-photos/9wzdncrfjbh4" - "Description" = "Photo and video editor" - } - "Microsoft.WindowsAlarms" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-alarms-clock/9wzdncrfj3pr" - "Description" = "A combination app, of alarm clock, world clock, timer, and stopwatch." - } - "Microsoft.WindowsCalculator" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-calculator/9wzdncrfhvn5" - "Description" = "Microsoft Calculator app" - } - "Microsoft.WindowsCamera" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-camera/9wzdncrfjbbg" - "Description" = "Camera app to manage photos and video" - } - "microsoft.windowscommunicationsapps" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/mail-and-calendar/9wzdncrfhvqm" - "Description" = "Mail & Calendar apps" - } - "Microsoft.WindowsFeedbackHub" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/feedback-hub/9nblggh4r32n" - "Description" = "App to provide Feedback on Windows and apps to Microsoft" - } - "Microsoft.WindowsMaps" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-maps/9wzdncrdtbvb" - "Description" = "Microsoft Maps app" - } - "Microsoft.WindowsNotepad" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-notepad/9msmlrh6lzf3" - "Description" = "Fast, simple text editor for plain text documents and source code files." - } - "Microsoft.WindowsStore" = @{ - "VDIState" = "Unchanged" - "URL" = "https://blogs.windows.com/windowsexperience/2021/06/24/building-a-new-open-microsoft-store-on-windows-11/" - "Description" = "Windows Store app" - } - "Microsoft.WindowsSoundRecorder" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-voice-recorder/9wzdncrfhwkn" - "Description" = "(Voice recorder)" - } - "Microsoft.WindowsTerminal" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701" - "Description" = "A terminal app featuring tabs, panes, Unicode, UTF-8 character support, and GPU text rendering engine." - } - "Microsoft.Winget.Platform.Source" = @{ - "VDIState" = "Unchanged" - "URL" = "https://learn.microsoft.com/en-us/windows/package-manager/winget/" - "Description" = "The Winget tool enables users to manage applications on Win10 and Win11 devices. This tool is the client interface to the Windows Package Manager service" - } - "Microsoft.Xbox.TCUI" = @{ - "VDIState" = "Unchanged" - "URL" = "https://docs.microsoft.com/en-us/gaming/xbox-live/features/general/tcui/live-tcui-overview" - "Description" = "XBox Title Callable UI (TCUI) enables your game code to call pre-defined user interface displays" - } - "Microsoft.XboxIdentityProvider" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/xbox-identity-provider/9wzdncrd1hkw" - "Description" = "A system app that enables PC games to connect to Xbox Live." - } - "Microsoft.XboxSpeechToTextOverlay" = @{ - "VDIState" = "Unchanged" - "URL" = "https://support.xbox.com/help/account-profile/accessibility/use-game-chat-transcription" - "Description" = "Xbox game transcription overlay" - } - "Microsoft.YourPhone" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/Your-phone/9nmpj99vjbwv" - "Description" = "Android phone to PC device interface app" - } - "Microsoft.ZuneMusic" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/groove-music/9wzdncrfj3pt" - "Description" = "Groove Music app" - } - "Microsoft.ZuneVideo" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/movies-tv/9wzdncrfj3p2" - "Description" = "Movies and TV app" - } - "MicrosoftCorporationII.QuickAssist" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/9P7BP5VNWKX5?hl=en-us&gl=US" - "Description" = "Microsoft remote help app" - } - "MicrosoftWindows.Client.WebExperience" = @{ - "VDIState" = "Unchanged" - "URL" = "" - "Description" = "Windows 11 Internet information widget" - } - "Microsoft.XboxApp" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/store/apps/9wzdncrfjbd8" - "Description" = "Xbox 'Console Companion' app (games, friends, etc.)" - } - "Microsoft.MixedReality.Portal" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/mixed-reality-portal/9ng1h8b3zc7m" - "Description" = "The app that facilitates Windows Mixed Reality setup, and serves as the command center for mixed reality experiences" - } - "Microsoft.Microsoft3DViewer" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/p/3d-viewer/9nblggh42ths" - "Description" = "App to view common 3D file types" - } - "MicrosoftTeams" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/xp8bt8dw290mpq" - "Description" = "Microsoft communication platform" - } - "MSTeams" = @{ - "VDIState" = "Unchanged" - "URL" = "https://apps.microsoft.com/detail/xp8bt8dw290mpq" - "Description" = "Microsoft communication platform" - } - "Microsoft.OneDriveSync" = @{ - "VDIState" = "Unchanged" - "URL" = "https://docs.microsoft.com/en-us/onedrive/one-drive-sync" - "Description" = "Microsoft OneDrive sync app (included in Office 2016 or later)" - } - "Microsoft.Wallet" = @{ - "VDIState" = "Unchanged" - "URL" = "https://www.microsoft.com/en-us/payments" - "Description" = "(Microsoft Pay) for Edge browser on certain devices" + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-solitaire-collection/9wzdncrfhwd2" + Description = "Solitaire suite of games" + } + + "Microsoft.MicrosoftStickyNotes" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-sticky-notes/9nblggh4qghw" + Description = "Note-taking app" + } + + "Microsoft.OutlookForWindows" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9NRX63209R7B?hl=en-us&gl=US" + Description = "A best-in-class email experience that is free for anyone with Windows" + } + + "Microsoft.MSPaint" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/store/detail/paint-3d/9NBLGGH5FV99" + Description = "Paint 3D app (not Classic Paint app)" + } + + "Microsoft.Paint" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9PCFS5B6T72H" + Description = "Classic Paint app" + } + + "Microsoft.People" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-people/9nblggh10pg8" + Description = "Contact management app" + } + + "Microsoft.PowerAutomateDesktop" = @{ + VDIState = "Unchanged" + URL = "https://flow.microsoft.com/en-us/desktop/" + Description = "Power Automate Desktop app. Record desktop and web actions in a single flow" + } + + "Microsoft.ScreenSketch" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/snip-sketch/9mz95kl8mr0l" + Description = "Snip and Sketch app" + } + + "Microsoft.SkypeApp" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/skype/9wzdncrfj364" + Description = "Instant message, voice or video call app" + } + + "Microsoft.StorePurchaseApp" = @{ + VDIState = "Unchanged" + URL = "" + Description = "Store purchase app helper" + } + + "Microsoft.Todos" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-to-do-lists-tasks-reminders/9nblggh5r558" + Description = "Microsoft To Do makes it easy to plan your day and manage your life" + } + + "Microsoft.WinDbg.Fast" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9PGJGD53TN86?hl=en-us&gl=US" + Description = "Microsoft WinDbg" + } + + "Microsoft.Windows.DevHome" = @{ + VDIState = "Unchanged" + URL = "https://learn.microsoft.com/en-us/windows/dev-home/" + Description = "A control center providing the ability to monitor projects in your dashboard using customizable widgets and more" + } + + "Microsoft.Windows.Photos" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/microsoft-photos/9wzdncrfjbh4" + Description = "Photo and video editor" + } + + "Microsoft.WindowsAlarms" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-alarms-clock/9wzdncrfj3pr" + Description = "A combination app of alarm clock, world clock, timer, and stopwatch." + } + + "Microsoft.WindowsCalculator" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-calculator/9wzdncrfhvn5" + Description = "Microsoft Calculator app" + } + + "Microsoft.WindowsCamera" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-camera/9wzdncrfjbbg" + Description = "Camera app to manage photos and video" + } + + "microsoft.windowscommunicationsapps" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/mail-and-calendar/9wzdncrfhvqm" + Description = "Mail & Calendar apps" + } + + "Microsoft.WindowsFeedbackHub" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/feedback-hub/9nblggh4r32n" + Description = "App to provide Feedback on Windows and apps to Microsoft" + } + + "Microsoft.WindowsMaps" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-maps/9wzdncrdtbvb" + Description = "Microsoft Maps app" + } + + "Microsoft.WindowsNotepad" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-notepad/9msmlrh6lzf3" + Description = "Fast, simple text editor for plain text documents and source code files." + } + + "Microsoft.WindowsStore" = @{ + VDIState = "Unchanged" + URL = "https://blogs.windows.com/windowsexperience/2021/06/24/building-a-new-open-microsoft-store-on-windows-11/" + Description = "Windows Store app" + } + + "Microsoft.WindowsSoundRecorder" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-voice-recorder/9wzdncrfhwkn" + Description = "(Voice recorder)" + } + + "Microsoft.WindowsTerminal" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/windows-terminal/9n0dx20hk701" + Description = "A terminal app featuring tabs, panes, Unicode, UTF-8 character support, and a GPU text rendering engine." + } + + "Microsoft.Winget.Platform.Source" = @{ + VDIState = "Unchanged" + URL = "https://learn.microsoft.com/en-us/windows/package-manager/winget/" + Description = "The Winget tool enables users to manage applications on Win10 and Win11 devices. This tool is the client interface to the Windows Package Manager service" + } + + "Microsoft.Xbox.TCUI" = @{ + VDIState = "Unchanged" + URL = "https://docs.microsoft.com/en-us/gaming/xbox-live/features/general/tcui/live-tcui-overview" + Description = "XBox Title Callable UI (TCUI) enables your game code to call pre-defined user interface displays" + } + + "Microsoft.XboxIdentityProvider" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/xbox-identity-provider/9wzdncrd1hkw" + Description = "A system app that enables PC games to connect to Xbox Live." + } + + "Microsoft.XboxSpeechToTextOverlay" = @{ + VDIState = "Unchanged" + URL = "https://support.xbox.com/help/account-profile/accessibility/use-game-chat-transcription" + Description = "Xbox game transcription overlay" + } + + "Microsoft.YourPhone" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/Your-phone/9nmpj99vjbwv" + Description = "Android phone to PC device interface app" + } + + "Microsoft.ZuneMusic" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/groove-music/9wzdncrfj3pt" + Description = "Groove Music app" + } + + "Microsoft.ZuneVideo" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/movies-tv/9wzdncrfj3p2" + Description = "Movies and TV app" + } + + "MicrosoftCorporationII.QuickAssist" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/9P7BP5VNWKX5?hl=en-us&gl=US" + Description = "Microsoft remote help app" + } + + "MicrosoftWindows.Client.WebExperience" = @{ + VDIState = "Unchanged" + URL = "" + Description = "Windows 11 Internet information widget" + } + + "Microsoft.XboxApp" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/store/apps/9wzdncrfjbd8" + Description = "Xbox 'Console Companion' app (games, friends, etc.)" + } + + "Microsoft.MixedReality.Portal" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/mixed-reality-portal/9ng1h8b3zc7m" + Description = "The app that facilitates Windows Mixed Reality setup, and serves as the command center for mixed reality experiences" + } + + "Microsoft.Microsoft3DViewer" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/p/3d-viewer/9nblggh42ths" + Description = "App to view common 3D file types" + } + + "MicrosoftTeams" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/xp8bt8dw290mpq" + Description = "Microsoft communication platform" + } + + "MSTeams" = @{ + VDIState = "Unchanged" + URL = "https://apps.microsoft.com/detail/xp8bt8dw290mpq" + Description = "Microsoft communication platform" + } + + "Microsoft.OneDriveSync" = @{ + VDIState = "Unchanged" + URL = "https://docs.microsoft.com/en-us/onedrive/one-drive-sync" + Description = "Microsoft OneDrive sync app (included in Office 2016 or later)" + } + + "Microsoft.Wallet" = @{ + VDIState = "Unchanged" + URL = "https://www.microsoft.com/en-us/payments" + Description = "(Microsoft Pay) for Edge browser on certain devices" } } -Foreach ($Key in $apps.Keys) { +foreach ($Key in $apps.Keys) { $Item = $apps[$Key] + Write-Host "Removing Provisioned Package $Key" Get-AppxProvisionedPackage -Online | - Where-Object { $_.PackageName -like ("*{0}*" -f $Key) } | - Remove-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | Out-Null + Where-Object { $_.PackageName -like ("*{0}*" -f $Key) } | + Remove-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue | + Out-Null Write-Host "Attempting to remove [All Users] $Key - $($Item.Description)" Get-AppxPackage -AllUsers -Name ("*{0}*" -f $Key) | - Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue + Remove-AppxPackage -AllUsers -ErrorAction SilentlyContinue Write-Host "Attempting to remove $Key - $($Item.Description)" Get-AppxPackage -Name ("*{0}*" -f $Key) | - Remove-AppxPackage -ErrorAction SilentlyContinue | Out-Null - + Remove-AppxPackage -ErrorAction SilentlyContinue | + Out-Null } diff --git a/modules/win_disable_services/manifests/disable_defender_smartscreen.pp b/modules/win_disable_services/manifests/disable_defender_smartscreen.pp new file mode 100644 index 000000000..c21bfe933 --- /dev/null +++ b/modules/win_disable_services/manifests/disable_defender_smartscreen.pp @@ -0,0 +1,50 @@ +class win_disable_services::disable_defender_smartscreen { + + ## 1) Shell/Explorer SmartScreen (policy) + registry_key { 'HKLM\SOFTWARE\Policies\Microsoft\Windows\System': + ensure => present, + } + + registry_value { 'HKLM\SOFTWARE\Policies\Microsoft\Windows\System\EnableSmartScreen': + ensure => present, + type => dword, + data => '0', + } + + registry_value { 'HKLM\SOFTWARE\Policies\Microsoft\Windows\System\ShellSmartScreenLevel': + ensure => absent, + } + + ## 2) Explorer non-policy setting (per NinjaOne) + registry_key { 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer': + ensure => present, + } + + registry_value { 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Explorer\SmartScreenEnabled': + ensure => present, + type => string, + data => 'Off', + } + + ## 3) Edge SmartScreen (official Edge policy) + registry_key { 'HKLM\SOFTWARE\Policies\Microsoft\Edge': + ensure => present, + } + + registry_value { 'HKLM\SOFTWARE\Policies\Microsoft\Edge\SmartScreenEnabled': + ensure => present, + type => dword, + data => '0', + } + + ## 4) Store apps / AppHost web content evaluation + registry_key { 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost': + ensure => present, + } + + registry_value { 'HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost\EnableWebContentEvaluation': + ensure => present, + type => dword, + data => '0', + } +} diff --git a/modules/win_disable_services/manifests/disable_ms_edge.pp b/modules/win_disable_services/manifests/disable_ms_edge.pp new file mode 100644 index 000000000..fb7a252fb --- /dev/null +++ b/modules/win_disable_services/manifests/disable_ms_edge.pp @@ -0,0 +1,10 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +class win_disable_services::disable_ms_edge { + win_disable_services::disable_service { 'edgeupdate': + } + win_disable_services::disable_service { 'MicrosoftEdgeElevationService': + } +} diff --git a/modules/win_disable_services/manifests/disable_optional_services.pp b/modules/win_disable_services/manifests/disable_optional_services.pp new file mode 100644 index 000000000..98b45c358 --- /dev/null +++ b/modules/win_disable_services/manifests/disable_optional_services.pp @@ -0,0 +1,78 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +## "- DON'T Disable." = has adverse effects if disabled + +class win_disable_services::disable_optional_services { + + $services = [ + + # --- Bluetooth --- + 'BTAGService', # Bluetooth Audio Gateway Service + 'bthserv', # Bluetooth Support Service + 'BthAvctpSvc', # AVCTP Service (Bluetooth audio) + + # --- Telemetry / diagnostics --- + 'DiagTrack', # Connected User Experiences and Telemetry + 'DPS', # Diagnostic Policy Service - Disabled in MaintainSytems script too + 'DusmSvc', # Data Usage + 'WdiServiceHost', # Diagnostic System Host + + # --- Network discovery & publishing --- + 'FDResPub', # Function Discovery Resource Publication + 'FDResHost', # Function Discovery Provider Host + + # --- Print / themes / prefetch (optional) --- + 'Spooler', # Print Spooler (disable only if you never print) + 'Themes', # Themes (visual styles) + 'SysMain', # SysMain (SuperFetch / prefetcher) + + # --- Wi-Fi / MS account / notifications / web accounts --- + 'WlanSvc', # WLAN AutoConfig (Wi-Fi) + 'wlidsvc', # Microsoft Account Sign-in Assistant + 'WpnService', # Windows Push Notifications System Service - Disabled in MaintainSytems script too + 'TokenBroker', # Web Account Manager + + # --- UWP / Microsoft Store ecosystem --- + 'AppReadiness', # App readiness +# 'AppXSvc', # AppX Deployment Service - won't disable +# 'CDPSvc', # Connected Devices Platform Service - DON'T Disable. +# 'ClipSVC', # Client License Service (Store licensing) - DON'T Disable. +# 'CoreMessagingRegistrar', # CoreMessaging - won't disable +# 'StateRepository', # State Repository Service - DON'T Disable. +# 'SystemEventsBroker', # System Events Broker - DON'T Disable. +# 'TextInputManagementSvc', # Text Input Management - DON'T Disable. +# 'TimeBrokerSvc', # Time Broker (background tasks) - DON'T Disable. + + # --- Indexing / contacts --- + 'TrkWks', # Distributed Link Tracking Client - Disabled in MaintainSytems script too + + # --- Third-party / vendor helpers (excluding nxlog) --- + 'igccservice', # Intel Graphics Command Center Service + 'IntelAudioService', # Intel Audio Service + 'jhi_service', # Intel Dynamic Application Loader Host + 'RtkAudioUniversalService', # Realtek Audio Universal Service +# 'webthreatdefsvc', # Web Threat Defense service - DON'T Disable. + + # --- Others --- +# 'RmSvc', # Radio Management Service (airplane mode / radios) - DON'T Disable. + 'NgcCtnrSvc', # Microsoft Passport Container (Windows Hello / PIN) + 'lfsvc', # Geolocation Service + 'PcaSvc', # Program Compatibility Assistant Service + 'SSDPSRV', # SSDP Discovery/UPnP Discovery + ] + + $services_disable_only = [ + 'webthreatdefsvc', + 'RmSvc', + ] + + service { $services: + enable => false, + } + + service { $services_disable_only: + enable => false, + } +} diff --git a/modules/win_disable_services/manifests/disable_sync_from_cloud.pp b/modules/win_disable_services/manifests/disable_sync_from_cloud.pp new file mode 100644 index 000000000..75ee23477 --- /dev/null +++ b/modules/win_disable_services/manifests/disable_sync_from_cloud.pp @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +class win_disable_services::disable_sync_from_cloud { + + # GPO: Computer Configuration > Administrative Templates > Windows Components > Sync your settings > Do not sync + # Registry: HKLM\SOFTWARE\Policies\Microsoft\Windows\SettingSync + # Effect: turns off "Remember my preferences" and none of the preferences are synced. :contentReference[oaicite:1]{index=1} + + registry_key { 'HKLM\SOFTWARE\Policies\Microsoft\Windows\SettingSync': + ensure => present, + } + + # DisableSettingSync: 2 = disable + registry_value { 'HKLM\SOFTWARE\Policies\Microsoft\Windows\SettingSync\DisableSettingSync': + ensure => present, + type => dword, + data => '2', + } + + # DisableSettingSyncUserOverride: 1 = prevent user override (keeps it off) + registry_value { 'HKLM\SOFTWARE\Policies\Microsoft\Windows\SettingSync\DisableSettingSyncUserOverride': + ensure => present, + type => dword, + data => '1', + } +} diff --git a/modules/win_disable_services/manifests/uninstall_appx_packages.pp b/modules/win_disable_services/manifests/uninstall_appx_packages.pp index 8efc57186..063ddbfee 100644 --- a/modules/win_disable_services/manifests/uninstall_appx_packages.pp +++ b/modules/win_disable_services/manifests/uninstall_appx_packages.pp @@ -1,8 +1,24 @@ # This class is responsible for disabling AppX packages on Windows. -class win_disable_services::uninstall_appx_packages { +class win_disable_services::uninstall_appx_packages ( + $apx_uninstall +){ + + $ronin_base = $facts['custom_win_roninprogramdata'] + $script_path = "${ronin_base}\\win_uninstall_appx_packages.ps1" + + file { $script_path: + ensure => file, + content => file("win_disable_services/appxpackages/${apx_uninstall}"), + } + exec { 'disable_appx_packages': - command => file('win_disable_services/appxpackages/uninstall.ps1'), - provider => powershell, - timeout => 300, + # Call the script file from PowerShell provider + command => "& '${script_path}'", + provider => powershell, + timeout => 300, + logoutput => true, + returns => [0], + require => File[$script_path], + tries => 1, } } diff --git a/modules/win_scheduled_tasks/files/BAKmaintainsystem-hw.ps1 b/modules/win_scheduled_tasks/files/BAKmaintainsystem-hw.ps1 new file mode 100644 index 000000000..2f4d49411 --- /dev/null +++ b/modules/win_scheduled_tasks/files/BAKmaintainsystem-hw.ps1 @@ -0,0 +1,609 @@ +<# +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +#> + +function Write-Log { + param ( + [string] $message, + [string] $severity = 'INFO', + [string] $source = 'MaintainSystem', + [string] $logName = 'Application' + ) + if (!([Diagnostics.EventLog]::Exists($logName)) -or !([Diagnostics.EventLog]::SourceExists($source))) { + New-EventLog -LogName $logName -Source $source + } + switch ($severity) { + 'DEBUG' { + $entryType = 'SuccessAudit' + $eventId = 2 + break + } + 'WARN' { + $entryType = 'Warning' + $eventId = 3 + break + } + 'ERROR' { + $entryType = 'Error' + $eventId = 4 + break + } + default { + $entryType = 'Information' + $eventId = 1 + break + } + } + Write-EventLog -LogName $logName -Source $source -EntryType $entryType -Category 0 -EventID $eventId -Message $message + if ([Environment]::UserInteractive) { + $fc = @{ 'Information' = 'White'; 'Error' = 'Red'; 'Warning' = 'DarkYellow'; 'SuccessAudit' = 'DarkGray' }[$entryType] + Write-Host -object $message -ForegroundColor $fc + } +} + +function Run-MaintainSystem { + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + #Remove-OldTaskDirectories + Get-ChildItem "$env:systemdrive\logs\old" -Recurse -File | Where-Object CreationTime -lt (Get-Date).AddDays(-7) | Remove-Item -Force + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} +function CompareConfigBasic { + param ( + [string]$yaml_url = "https://raw.githubusercontent.com/mozilla-platform-ops/worker-images/refs/heads/main/provisioners/windows/MDC1Windows/pools.yml", + [string]$PAT + ) + + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + + process { + $yaml = $null + $SETPXE = $false + $yamlHash = $null + $IPAddress = $null + + $Ethernet = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | + Where-Object { $_.Name -match "ethernet" } + + try { + $IPAddress = ($Ethernet.GetIPProperties().UnicastAddresses | + Where-Object { $_.Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.Address.IPAddressToString -ne "127.0.0.1" } | + Select-Object -First 1 -ExpandProperty Address).IPAddressToString + } + catch { + try { + $NetshOutput = netsh interface ip show addresses + $IPAddress = ($NetshOutput -match "IP Address" | ForEach-Object { + if ($_ -notmatch "127.0.0.1") { $_ -replace ".*?:\s*", "" } + })[0] + } + catch { + Write-Log -message "Failed to get IP address" -severity 'ERROR' + } + } + + if (-not $IPAddress) { + Write-Log -message "No IP Address could be determined." -severity 'ERROR' + Restart-Computer -Force + return + } + + Write-Log -message "IP Address: $IPAddress" -severity 'INFO' + + try { + $ResolvedName = (Resolve-DnsName -Name $IPAddress -Server "10.48.75.120").NameHost + } + catch { + Write-Log -message "DNS resolution failed." -severity 'ERROR' + Restart-Computer -Force + return + } + + Write-Log -message "Resolved Name: $ResolvedName" -severity 'INFO' + + $index = $ResolvedName.IndexOf('.') + if ($index -lt 0) { + Write-Log -message "Invalid hostname format." -severity 'ERROR' + Restart-Computer -Force + return + } + + $worker_node_name = $ResolvedName.Substring(0, $index) + Write-Log -message "Host name set to: $worker_node_name" -severity 'INFO' + + $localHash = (Get-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet).GITHASH + $localPool = (Get-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet).worker_pool_id + + $maxRetries = 5 + $retryDelay = 10 + $attempt = 0 + $success = $false + + while ($attempt -lt $maxRetries -and -not $success) { + try { + $Headers = @{ + Accept = "application/vnd.github+json" + Authorization = "Bearer $($PAT)" + "X-GitHub-Api-Version" = "2022-11-28" + } + $response = Invoke-WebRequest -Uri $yaml_url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop -Headers $Headers + $yaml = $response.Content | ConvertFrom-Yaml + + if ($yaml) { + $success = $true + } + else { + throw "YAML content empty" + } + } + catch { + Write-Log -message "Attempt $($attempt + 1): Failed to fetch YAML - $_" -severity 'WARN' + Start-Sleep -Seconds $retryDelay + $attempt++ + } + } + + if (-not $success) { + Write-Log -message "YAML could not be loaded. Forcing PXE + reboot." -severity 'ERROR' + Set-PXE + Restart-Computer -Force + return + } + + $found = $false + foreach ($pool in $yaml.pools) { + if ($pool.nodes -contains $worker_node_name) { + $WorkerPool = $pool.name + $yamlHash = $pool.hash + $yamlImageName = $pool.image + $yamlImageDir = "D:\" + $yamlImageName + $found = $true + break + } + } + + if (-not $found) { + Write-Log -message "Node not found in YAML. Forcing PXE + reboot." -severity 'ERROR' + Set-PXE + Restart-Computer -Force + return + } + + Write-Log -message "=== Configuration Comparison ===" -severity 'INFO' + + if ($localPool -ne $WorkerPool) { + Write-Log -message "Worker Pool MISMATCH!" -severity 'ERROR' + $SETPXE = $true + } + else { + Write-Log -message "Worker Pool Match: $WorkerPool" -severity 'INFO' + } + + if ([string]::IsNullOrWhiteSpace($yamlHash) -or $localHash -ne $yamlHash) { + Write-Log -message "Git Hash MISMATCH or missing YAML hash!" -severity 'ERROR' + Write-Log -message "Local: $localHash" -severity 'WARN' + Write-Log -message "YAML : $yamlHash" -severity 'WARN' + $SETPXE = $true + } + else { + Write-Log -message "Git Hash Match: $yamlHash" -severity 'INFO' + } + + if (!(Test-Path $yamlImageDir)) { + Write-Log -message "Image directory missing: $yamlImageDir" -severity 'ERROR' + $SETPXE = $true + } + + if ($SETPXE) { + Write-Log -message "Configuration mismatch — initiating PXE + reboot." -severity 'ERROR' + Set-PXE + Restart-Computer -Force + return + } + + Write-Log -message "Configuration is correct. No reboot required." -severity 'INFO' + } + + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} +function Remove-OldTaskDirectories { + param ( + [string[]] $targets = @('Z:\task_*', 'C:\Users\task_*') + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + foreach ($target in ($targets | Where-Object { (Test-Path -Path ('{0}:\' -f $_[0]) -ErrorAction SilentlyContinue) })) { + $all_task_paths = @(Get-ChildItem -Path $target | Sort-Object -Property { $_.LastWriteTime }) + if ($all_task_paths.length -gt 1) { + Write-Log -message ('{0} :: {1} task directories detected matching pattern: {2}' -f $($MyInvocation.MyCommand.Name), $all_task_paths.length, $target) -severity 'INFO' + $old_task_paths = $all_task_paths[0..($all_task_paths.Length - 2)] + foreach ($old_task_path in $old_task_paths) { + try { + & takeown.exe @('/a', '/f', $old_task_path, '/r', '/d', 'Y') + & icacls.exe @($old_task_path, '/grant', 'Administrators:F', '/t') + Remove-Item -Path $old_task_path -Force -Recurse + Write-Log -message ('{0} :: removed task directory: {1}, with last write time: {2}' -f $($MyInvocation.MyCommand.Name), $old_task_path.FullName, $old_task_path.LastWriteTime) -severity 'INFO' + } + catch { + Write-Log -message ('{0} :: failed to remove task directory: {1}, with last write time: {2}. {3}' -f $($MyInvocation.MyCommand.Name), $old_task_path.FullName, $old_task_path.LastWriteTime, $_.Exception.Message) -severity 'ERROR' + } + } + } + elseif ($all_task_paths.length -eq 1) { + Write-Log -message ('{0} :: a single task directory was detected at: {1}, with last write time: {2}' -f $($MyInvocation.MyCommand.Name), $all_task_paths[0].FullName, $all_task_paths[0].LastWriteTime) -severity 'DEBUG' + } + else { + Write-Log -message ('{0} :: no task directories detected matching pattern: {1}' -f $($MyInvocation.MyCommand.Name), $target) -severity 'DEBUG' + } + } + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} +function Check-RoninNodeOptions { + param ( + [string] $inmutable = (Get-ItemProperty -path "HKLM:\SOFTWARE\Mozilla\ronin_puppet").inmutable, + [string] $flagfile = "$env:programdata\PuppetLabs\ronin\semaphore\task-claim-state.valid" + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + Write-Host $inmutable + if ($inmutable -eq 'true') { + Write-Log -message ('{0} :: Node is set to be inmutable' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Remove-Item -path $lock -ErrorAction SilentlyContinue + write-host New-item -path $flagfile + Exit-PSSession + } + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} + +Function UpdateRonin { + param ( + [string] $sourceOrg, + [string] $sourceRepo, + [string] $sourceBranch, + [string] $ronin_repo = "$env:systemdrive\ronin" + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + $sourceOrg = $(if ((Test-Path -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -ErrorAction SilentlyContinue) -and (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Organisation' -ErrorAction SilentlyContinue)) { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Organisation').Organisation } else { 'mozilla-platform-ops' }) + $sourceRepo = $(if ((Test-Path -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -ErrorAction SilentlyContinue) -and (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Repository' -ErrorAction SilentlyContinue)) { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Repository').Repository } else { 'ronin_puppet' }) + $sourceBranch = $(if ((Test-Path -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -ErrorAction SilentlyContinue) -and (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Branch' -ErrorAction SilentlyContinue)) { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Branch').Branch } else { 'master' }) + + Set-Location $ronin_repo + git config --global --add safe.directory "C:/ronin" + git pull https://github.com/$sourceOrg/$sourceRepo $sourceBranch + $git_exit = $LastExitCode + if ($git_exit -eq 0) { + $git_hash = (git rev-parse --verify HEAD) + Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name githash -type string -value $git_hash + Write-Log -message ('{0} :: Checking/pulling updates from https://github.com/{1}/{2}. Branch: {3}.' -f $($MyInvocation.MyCommand.Name), ($sourceOrg), ($sourceRepo), ($sourceRev)) -severity 'DEBUG' + } + else { + # Fall back to clone if pull fails + Write-Log -message ('{0} :: Git pull failed! https://github.com/{1}/{2}. Branch: {3}.' -f $($MyInvocation.MyCommand.Name), ($sourceOrg), ($sourceRepo), ($sourceRev)) -severity 'DEBUG' + Write-Log -message ('{0} :: Deleting old repository and cloning repository .' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Move-item -Path $ronin_repo\manifests\nodes.pp -Destination $env:TEMP\nodes.pp + Move-item -Path $ronin_repo\data\secrets\vault.yaml -Destination $env:TEMP\vault.yaml + #Remove-Item -Recurse -Force $ronin_repo + Start-Sleep -s 2 + git clone --single-branch --branch $sourceRev https://github.com/$sourceOrg/$sourceRepo $ronin_repo + Move-item -Path $env:TEMP\nodes.pp -Destination $ronin_repo\manifests\nodes.pp + Move-item -Path $env:TEMP\vault.yaml -Destination $ronin_repo\data\secrets\vault.yaml + } + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} +function Puppet-Run { + param ( + [int] $exit, + [string] $lock = "$env:programdata\PuppetLabs\ronin\semaphore\ronin_run.lock", + [int] $last_exit = (Get-ItemProperty "HKLM:\SOFTWARE\Mozilla\ronin_puppet").last_run_exit, + [string] $run_to_success = (Get-ItemProperty "HKLM:\SOFTWARE\Mozilla\ronin_puppet").runtosuccess, + [string] $nodes_def = "$env:systemdrive\ronin\manifests\nodes\odes.pp", + [string] $logdir = "$env:systemdrive\logs", + [string] $fail_dir = "$env:systemdrive\fail_logs", + [string] $log_file = "$datetime-puppetrun.log", + [string] $datetime = (get-date -format yyyyMMdd-HHmm), + [string] $flagfile = "$env:programdata\PuppetLabs\ronin\semaphore\task-claim-state.valid" + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + + Check-RoninNodeOptions + UpdateRonin + + # Setting Env variabes for PuppetFile install and Puppet run + # The ssl variables are needed for R10k + Write-Log -message ('{0} :: Setting Puppet enviroment.' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + $env:path = "$($env:ProgramFiles)\Puppet Labs\Puppet\puppet\bin;$($env:ProgramFiles)\Puppet Labs\Puppet\bin;$env:path" + + $env:SSL_CERT_FILE = "$($env:ProgramFiles)\Puppet Labs\Puppet\puppet\ssl\cert.pem" + $env:SSL_CERT_DIR = "$($env:ProgramFiles)\Puppet Labs\Puppet\puppet\ssl" + $env:FACTER_env_windows_installdir = "$($env:ProgramFiles)\Puppet Labs\Puppet" + $env:PL_BASEDIR = "$($env:ProgramFiles)\Puppet Labs\Puppet" + $env:PUPPET_DIR = "$($env:ProgramFiles)\Puppet Labs\Puppet" + $env:RUBYLIB = "$($env:ProgramFiles)\Puppet Labs\Puppet\lib" + + $env:USERNAME = "Administrator" + $env:USERPROFILE = "$env:systemdrive\Users\Administrator" + + # This is temporary and should be removed after the cloud_windows branch is merged + # Hiera lookups will fail after the merge if this is not in place following the merge + <# + if((test-path $env:systemdrive\ronin\win_hiera.yaml)) { + $hiera = "win_hiera.yaml" + } else { + $hiera = "hiera.yaml" + } + #> + # this will break Win 10 1803 if this is merged into the master brnach + $hiera = "hiera.yaml" + + # Needs to be removed from path or a wrong puppet file will be used + $env:path = ($env:path.Split(';') | Where-Object { $_ -ne "$env:programfiles\Puppet Labs\Puppet\puppet\bin" }) -join ';' + If (!(test-path $fail_dir)) { + New-Item -ItemType Directory -Force -Path $fail_dir + } + Get-ChildItem -Path $logdir\*.log -Recurse | Move-Item -Destination $logdir\old -ErrorAction SilentlyContinue + Write-Log -message ('{0} :: Initiating Puppet apply .' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + puppet apply manifests\nodes.pp --onetime --verbose --no-daemonize --no-usecacheonfailure --detailed-exitcodes --no-splay --show_diff --modulepath=modules`;r10k_modules --hiera_config=$hiera --logdest $logdir\$log_file + [int]$puppet_exit = $LastExitCode + + if ($run_to_success -eq 'true') { + if (($puppet_exit -ne 0) -and ($puppet_exit -ne 2)) { + if ($last_exit -eq 0) { + Write-Log -message ('{0} :: Puppet apply failed.' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $puppet_exit + Remove-Item $lock -ErrorAction SilentlyContinue + # If the Puppet run fails send logs to papertrail + # Nxlog watches $fail_dir for files names *-puppetrun.log + Move-Item $logdir\$log_file -Destination $fail_dir + shutdown @('-r', '-t', '0', '-c', 'Reboot; Puppet apply failed', '-f', '-d', '4:5') + } + elseif ($last_exit -ne 0) { + Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $puppet_exit + Remove-Item $lock + Move-Item $logdir\$log_file -Destination $fail_dir + Write-Log -message ('{0} :: Puppet apply failed. Waiting 10 minutes beofre Reboot' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Start-Sleep 600 + shutdown @('-r', '-t', '0', '-c', 'Reboot; Puppet apply failed', '-f', '-d', '4:5') + } + } + elseif (($puppet_exit -match 0) -or ($puppet_exit -match 2)) { + Write-Log -message ('{0} :: Puppet apply successful' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $puppet_exit + Remove-Item -path $lock + New-item -path $flagfile + } + else { + Write-Log -message ('{0} :: Unable to detrimine state post Puppet apply' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $last_exit + Move-Item $logdir\$log_file -Destination $fail_dir + Remove-Item -path $lock + shutdown @('-r', '-t', '600', '-c', 'Reboot; Unveriable state', '-f', '-d', '4:5') + } + } + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} + +function StartWorkerRunner { + param ( + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + ## Checking for issues with the user profile. + $lastBootTime = Get-WinEvent -LogName "System" -FilterXPath "" | + Select-Object -First 1 | + ForEach-Object { $_.TimeCreated } + $eventIDs = @(1511, 1515) + + $events = Get-WinEvent -LogName "Application" | + Where-Object { $_.ID -in $eventIDs -and $_.TimeCreated -gt $lastBootTime } | + Sort-Object TimeCreated -Descending | Select-Object -First 1 + + if ($events) { + Write-Log -message ('{0} :: Possible User Profile Corruption. Restarting' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Restart-Computer -Force + exit + } + Start-Service -Name worker-runner + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} + +function Get-LoggedInUser { + [CmdletBinding()] + param ( + + ) + + @(((query user) -replace '\s{20,39}', ',,') -replace '\s{2,}', ',' | ConvertFrom-Csv) +} + +function Get-LatestGoogleChrome { + [CmdletBinding()] + param ( + [String] + $Package = "googlechrome" + ) + + ## Current version of google chrome + $current_version = choco list --exact $Package --limit-output | ConvertFrom-Csv -Delimiter '|' -Header 'Name', 'CurrentVersion' + + ## Use chocolatey with outdated + $choco_packages = choco outdated --limit-output | ConvertFrom-Csv -Delimiter '|' -Header 'Name', 'CurrentVersion', 'AvailableVersion', 'Pinned' + + ## Check if Google Chrome is present + $pkg = $choco_packages | Where-Object { $_.Name -eq $Package } + + ## There is no google chrome update, so output the current version + if ([String]::IsNullOrEmpty($pkg)) { + Write-Log -message ('{0} :: Google Chrome version installed is {1}' -f $($MyInvocation.MyCommand.Name), $current_version.CurrentVersion) -severity 'DEBUG' + } + else { + ## Chrome is installed and needs to be updated + if ($pkg.CurrentVersion -ne $pkg.AvailableVersion) { + ## run choco upgrade + Write-Log -message ('{0} :: Updating Google Chrome from current: {1} to available: {2}' -f $($MyInvocation.MyCommand.Name), $pkg.currentVersion, $pkg.availableVersion) -severity 'DEBUG' + choco upgrade $Package -y "--ignore-checksums" "--ignore-package-exit-codes" "--log-file" $env:systemdrive\logs\googlechrome.log + if ($LASTEXITCODE -ne 0) { + ## output to papertrail + Write-Log -message ('{0} :: choco upgrade googlechrome failed with {1}' -f $($MyInvocation.MyCommand.Name), $LASTEXITCODE) -severity 'DEBUG' + ## output chocolatey logs to papertrail + Get-Content $env:systemdrive\logs\googlechrome.log | ForEach-Object { Write-Log -message $_ -severity 'DEBUG' } + ## Sending the logs to papertrail, wait 30 seconds + Start-Sleep -Seconds 60 + ## PXE Boot + Set-PXE + } + else { + ## Need to reboot in order to complete the upgrade + Write-Log -message ('{0} :: Google Chrome needs to reboot to complete upgrade. Rebooting..' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Start-Sleep -Seconds 10 + Restart-Computer -Force + } + } + } +} + +function Set-PXE { + param ( + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + $temp_dir = "$env:systemdrive\temp\" + New-Item -ItemType Directory -Force -Path $temp_dir -ErrorAction SilentlyContinue + + bcdedit /enum firmware > $temp_dir\firmware.txt + + $fwbootmgr = Select-String -Path "$temp_dir\firmware.txt" -Pattern "{fwbootmgr}" + if (!$fwbootmgr) { + Write-Log -message ('{0} :: Device is configured for Legacy Boot. Exiting!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Exit 999 + } + Try { + # Get the line of text with the GUID for the PXE boot option. + # IPV4 = most PXE boot options + $FullLine = (( Get-Content $temp_dir\firmware.txt | Select-String "IPV4|EFI Network" -Context 1 -ErrorAction Stop ).context.precontext)[0] + + # Remove all text but the GUID + $GUID = '{' + $FullLine.split('{')[1] + + # Add the PXE boot option to the top of the boot order on next boot + bcdedit /set "{fwbootmgr}" bootsequence "$GUID" + + Write-Log -message ('{0} :: Device will PXE boot. Restarting' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Restart-Computer -Force + } + Catch { + Write-Log -message ('{0} :: Unable to set next boot to PXE. Exiting!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Exit 888 + } + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} + +function Test-ConnectionUntilOnline { + param ( + [string]$Hostname = "www.google.com", + [int]$Interval = 5, + [int]$TotalTime = 120 + ) + + $elapsedTime = 0 + + while ($elapsedTime -lt $totalTime) { + if (Test-Connection -ComputerName $hostname -Count 1 -Quiet) { + Write-Log -message ('{0} :: {1} is online! Continuing.' -f $($MyInvocation.MyCommand.Name), $ENV:COMPUTERNAME) -severity 'DEBUG' + return + } + else { + Write-Log -message ('{0} :: {1} is not online, checking again in {2}' -f $($MyInvocation.MyCommand.Name), $ENV:COMPUTERNAME, $interval) -severity 'DEBUG' + Start-Sleep -Seconds $interval + $elapsedTime += $interval + } + } + + Write-Log -message ('{0} :: {1} did not come online within {2} seconds' -f $($MyInvocation.MyCommand.Name), $ENV:COMPUTERNAME, $totalTime) -severity 'DEBUG' + throw "Connection timeout." +} + +## Bug https://bugzilla.mozilla.org/show_bug.cgi?id=1910123 +## The bug tracks when we reimaged a machine and the machine had a different refresh rate (64hz vs 60hz) +## This next line will check if the refresh rate is not 60hz and trigger a reimage if so +$hardware = Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -Property Manufacturer, Model +$model = $hardware.Model +$refresh_rate = (Get-WmiObject win32_videocontroller).CurrentRefreshRate +if ($refresh_rate -ne "60") { + Write-Log -message ('{0} :: Refresh rate is {1}. Reimaging {2}' -f $($MyInvocation.MyCommand.Name), $refresh_rate, $ENV:COMPUTERNAME) -severity 'DEBUG' + Set-PXE +} + +$bootstrap_stage = (Get-ItemProperty -path "HKLM:\SOFTWARE\Mozilla\ronin_puppet").bootstrap_stage +If ($bootstrap_stage -eq 'complete') { + CompareConfigBasic + Start-Sleep -Seconds 2 + Run-MaintainSystem + ## We're getting user profile corruption errors, so let's check that the user is logged in using quser.exe + for ($i = 0; $i -lt 3; $i++) { + $loggedInUser = (Get-LoggedInUser).UserName -replace ">" + if ($loggedInUser -notmatch "task") { + Write-Log -message ('{0} :: User logged in: {1}' -f $($MyInvocation.MyCommand.Name), $loggedInUser) -severity 'DEBUG' + Start-Sleep -Seconds 10 + } + else { + Write-Log -message ('{0} :: User logged in: {1}' -f $($MyInvocation.MyCommand.Name), $loggedInUser) -severity 'DEBUG' + break + } + } + + ## Let's make sure the machine is online before checking the internet + Test-ConnectionUntilOnline + + ## Let's check for the latest install of google chrome using chocolatey before starting worker runner + ## Instead of querying chocolatey each time this runs, let's query chrome json endoint and check locally installed version + Get-LatestGoogleChrome + + StartWorkerRunner +} +else { + Write-Log -message ('{0} :: Bootstrap has not completed. EXITING!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Exit-PSSession +} diff --git a/modules/win_scheduled_tasks/files/at_task_user_logon.ps1 b/modules/win_scheduled_tasks/files/at_task_user_logon.ps1 index 114302967..34a43ef4e 100644 --- a/modules/win_scheduled_tasks/files/at_task_user_logon.ps1 +++ b/modules/win_scheduled_tasks/files/at_task_user_logon.ps1 @@ -46,6 +46,499 @@ function Write-Log { } } +function Remove-OneDriveScheduledTasks { + [CmdletBinding()] + param( + [int]$TimeoutSeconds = 180, + [int]$RetryIntervalSeconds = 10, + [int]$PerTaskDeleteTimeoutSeconds = 60, + [int]$PerTaskRetryIntervalSeconds = 3 + ) + ## give it a minute to for schd task to be available + start-sleep -s 60 + function Get-OneDriveTaskNames { + try { + $rows = @(schtasks.exe /Query /FO CSV /V 2>$null | ConvertFrom-Csv) + if (-not $rows -or $rows.Count -eq 0) { return @() } + + $matches = $rows | Where-Object { + ($_.TaskName -match '(?i)onedrive') -or + (($_.'Task To Run') -and (($_.'Task To Run') -match '(?i)onedrive(\\.exe)?')) -or + (($_.Actions) -and ($_.Actions -match '(?i)onedrive(\\.exe)?')) -or + (($_.'Task Run') -and (($_.'Task Run') -match '(?i)onedrive(\\.exe)?')) -or + (($_.Actions) -and ($_.Actions -match '(?i)OneDriveSetup\.exe|\\OneDrive\.exe')) -or + (($_.'Task To Run') -and (($_.'Task To Run') -match '(?i)OneDriveSetup\.exe|\\OneDrive\.exe')) + } + + return @($matches | Select-Object -ExpandProperty TaskName -Unique) + } + catch { + Write-Log -message ("OneDriveTasks :: enumerate failed: {0}" -f $_.Exception.Message) -severity 'WARN' + return @() + } + } + + function Test-TaskExists([string]$TaskName) { + try { + schtasks.exe /Query /TN "$TaskName" 1>$null 2>$null + return ($LASTEXITCODE -eq 0) + } catch { + return $true # assume it exists if we couldn't query + } + } + + function Remove-TaskWithRetries { + param( + [Parameter(Mandatory)][string]$TaskName + ) + + $deadline = (Get-Date).AddSeconds($PerTaskDeleteTimeoutSeconds) + $attempt = 0 + + while ((Get-Date) -lt $deadline) { + $attempt++ + + try { + schtasks.exe /Delete /TN "$TaskName" /F 2>$null | Out-Null + $exit = $LASTEXITCODE + + if ($exit -eq 0) { + # Some tasks "delete" but linger briefly; verify + if (-not (Test-TaskExists -TaskName $TaskName)) { + Write-Log -message ("OneDriveTasks :: deleted {0} (attempt {1})" -f $TaskName, $attempt) -severity 'INFO' + return $true + } + + Write-Log -message ("OneDriveTasks :: delete reported success but task still exists: {0} (attempt {1})" -f $TaskName, $attempt) -severity 'WARN' + } else { + Write-Log -message ("OneDriveTasks :: delete failed {0} (exit {1}, attempt {2})" -f $TaskName, $exit, $attempt) -severity 'WARN' + } + } + catch { + Write-Log -message ("OneDriveTasks :: exception deleting {0} (attempt {1}): {2}" -f $TaskName, $attempt, $_.Exception.Message) -severity 'WARN' + } + + Start-Sleep -Seconds $PerTaskRetryIntervalSeconds + } + + Write-Log -message ("OneDriveTasks :: timeout deleting {0} after {1}s" -f $TaskName, $PerTaskDeleteTimeoutSeconds) -severity 'ERROR' + return $false + } + + Write-Log -message ("OneDriveTasks :: begin (timeout={0}s, interval={1}s, perTaskTimeout={2}s)" -f $TimeoutSeconds, $RetryIntervalSeconds, $PerTaskDeleteTimeoutSeconds) -severity 'DEBUG' + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $pass = 0 + + while ((Get-Date) -lt $deadline) { + $pass++ + $targets = Get-OneDriveTaskNames + + if (-not $targets -or $targets.Count -eq 0) { + Write-Log -message ("OneDriveTasks :: none found (pass {0})" -f $pass) -severity 'INFO' + Write-Log -message "OneDriveTasks :: end (success)" -severity 'DEBUG' + return + } + + Write-Log -message ("OneDriveTasks :: pass {0}: found {1} task(s)" -f $pass, $targets.Count) -severity 'INFO' + + foreach ($tn in $targets) { + $null = Remove-TaskWithRetries -TaskName $tn + } + + # Re-check right away; if still present, sleep and retry until overall timeout + $stillThere = Get-OneDriveTaskNames + if (-not $stillThere -or $stillThere.Count -eq 0) { + Write-Log -message ("OneDriveTasks :: verification OK after pass {0}" -f $pass) -severity 'INFO' + Write-Log -message "OneDriveTasks :: end (success)" -severity 'DEBUG' + return + } + + $remaining = [math]::Max(0, [int]($deadline - (Get-Date)).TotalSeconds) + Write-Log -message ("OneDriveTasks :: still present after pass {0} (remaining {1}s). Sleeping {2}s..." -f $pass, $remaining, $RetryIntervalSeconds) -severity 'WARN' + Start-Sleep -Seconds $RetryIntervalSeconds + } + + $final = Get-OneDriveTaskNames + if ($final -and $final.Count -gt 0) { + $sample = ($final | Select-Object -First 10) -join '; ' + Write-Log -message ("OneDriveTasks :: timeout after {0}s. Remaining task(s): {1}" -f $TimeoutSeconds, $sample) -severity 'ERROR' + } else { + Write-Log -message "OneDriveTasks :: end (success at timeout boundary)" -severity 'INFO' + } +} + +function Disable-OneDriveBackupPopup { + [CmdletBinding()] + param() + + Write-Log -message "Disable-OneDriveBackupPopup :: begin" -severity 'INFO' + + try { + $wb = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsBackup' + New-Item -Path $wb -Force | Out-Null + New-ItemProperty -Path $wb -Name 'DisableMonitoring' -PropertyType DWord -Value 1 -Force | Out-Null + Write-Log -message "Disable-OneDriveBackupPopup :: Set WindowsBackup DisableMonitoring=1" -severity 'INFO' + } catch { + Write-Log -message ("Disable-OneDriveBackupPopup :: Failed setting DisableMonitoring: {0}" -f $_.Exception.Message) -severity 'WARN' + } + + try { + $odPol = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\OneDrive' + New-Item -Path $odPol -Force | Out-Null + New-ItemProperty -Path $odPol -Name 'DisableFileSyncNGSC' -PropertyType DWord -Value 1 -Force | Out-Null + Write-Log -message "Disable-OneDriveBackupPopup :: Set OneDrive DisableFileSyncNGSC=1" -severity 'INFO' + } catch { + Write-Log -message ("Disable-OneDriveBackupPopup :: Failed setting DisableFileSyncNGSC: {0}" -f $_.Exception.Message) -severity 'WARN' + } + + try { + Get-Process -Name OneDrive -ErrorAction SilentlyContinue | ForEach-Object { + Write-Log -message ("Disable-OneDriveBackupPopup :: Stopping OneDrive.exe (Id={0})" -f $_.Id) -severity 'INFO' + Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue + } + } catch { + Write-Log -message ("Disable-OneDriveBackupPopup :: Failed stopping OneDrive process: {0}" -f $_.Exception.Message) -severity 'WARN' + } + + function Remove-RunEntry([string]$HiveRoot) { + $runKey = "${HiveRoot}\Software\Microsoft\Windows\CurrentVersion\Run" + foreach ($name in @('OneDrive','OneDriveSetup','Microsoft OneDrive')) { + try { + & reg.exe delete $runKey /v $name /f 1>$null 2>$null + } catch { } + } + } + + $defaultNtUser = 'C:\Users\Default\NTUSER.DAT' + if (Test-Path $defaultNtUser) { + try { + & reg.exe load 'HKU\DefaultUser' $defaultNtUser 1>$null 2>$null + Remove-RunEntry 'HKU\DefaultUser' + & reg.exe unload 'HKU\DefaultUser' 1>$null 2>$null + Write-Log -message "Disable-OneDriveBackupPopup :: Cleared OneDrive Run entries in Default user profile" -severity 'INFO' + } catch { + Write-Log -message ("Disable-OneDriveBackupPopup :: Failed editing Default user hive: {0}" -f $_.Exception.Message) -severity 'WARN' + try { & reg.exe unload 'HKU\DefaultUser' 1>$null 2>$null } catch { } + } + } else { + Write-Log -message "Disable-OneDriveBackupPopup :: Default NTUSER.DAT not found; skipping default profile edit" -severity 'DEBUG' + } + + try { + $userSids = @(Get-ChildItem Registry::HKEY_USERS -ErrorAction SilentlyContinue | + Where-Object { $_.PSChildName -match '^S-1-5-21-' } | + Select-Object -ExpandProperty PSChildName) + + foreach ($sid in $userSids) { + Remove-RunEntry ("HKU\{0}" -f $sid) + } + + Write-Log -message ("Disable-OneDriveBackupPopup :: Cleared OneDrive Run entries in {0} loaded user hive(s)" -f $userSids.Count) -severity 'INFO' + } catch { + Write-Log -message ("Disable-OneDriveBackupPopup :: Failed clearing loaded user hives: {0}" -f $_.Exception.Message) -severity 'WARN' + } + + Write-Log -message "Disable-OneDriveBackupPopup :: complete (recommend reboot)" -severity 'INFO' +} + +function Disable-PerUserUwpServices { + [CmdletBinding()] + param ( + [string[]] + $ServicePrefixes = @( + 'cbdhsvc_', # Clipboard User Service + 'OneSyncSvc_', # Sync Host + 'UdkUserSvc_', # Udk User Service + 'PimIndexMaintenanceSvc_', # Contact/People indexing + 'UnistoreSvc_', # User Data Storage + 'UserDataSvc_', # User Data Access + 'CDPUserSvc_', # Connected Devices Platform (user) + 'WpnUserService_', # Push Notifications (user) + 'webthreatdefusersvc_' # Web Threat Defense (user) + ) + ) + + foreach ($prefix in $ServicePrefixes) { + + $svcList = Get-Service -Name "$prefix*" -ErrorAction SilentlyContinue + + if (-not $svcList) { + Write-Log -message ('{0} :: No services found for prefix {1}' -f $($MyInvocation.MyCommand.Name), $prefix) -severity 'DEBUG' + continue + } + + foreach ($svc in $svcList) { + try { + if ($svc.Status -eq 'Running') { + Write-Log -message ('{0} :: Stopping per-user service {1}' -f $($MyInvocation.MyCommand.Name), $svc.Name) -severity 'DEBUG' + Stop-Service -Name $svc.Name -Force -ErrorAction Stop + } + else { + Write-Log -message ('{0} :: Service {1} is already {2}, no action needed' -f $($MyInvocation.MyCommand.Name), $svc.Name, $svc.Status) -severity 'DEBUG' + } + } + catch { + Write-Log -message ('{0} :: Failed to stop service {1}: {2}' -f $($MyInvocation.MyCommand.Name), $svc.Name, $_.Exception.Message) -severity 'DEBUG' + } + } + } +} + +function Remove-EdgeScheduledTasks { + [CmdletBinding()] + param( + [int]$TimeoutSeconds = 180, + [int]$RetryIntervalSeconds = 10, + [int]$PerTaskDeleteTimeoutSeconds = 60, + [int]$PerTaskRetryIntervalSeconds = 3 + ) + + # Match only the common Edge updater tasks (keeps this "safe-simple") + $NamePatterns = @( + '(?i)\\MicrosoftEdgeUpdateTaskMachineCore', + '(?i)\\MicrosoftEdgeUpdateTaskMachineUA', + '(?i)\\MicrosoftEdgeUpdateTaskMachine', # some builds vary + '(?i)\\EdgeUpdate' # fallback + ) + + $ActionPatterns = @( + '(?i)msedgeupdate\.exe', + '(?i)microsoftedgeupdate\.exe', + '(?i)edgeupdate\.exe' + ) + + function Get-EdgeTaskNames { + try { + $rows = @(schtasks.exe /Query /FO CSV /V 2>$null | ConvertFrom-Csv) + if (-not $rows -or $rows.Count -eq 0) { return @() } + + $matches = $rows | Where-Object { + $tn = $_.TaskName + $a1 = $_.'Task To Run' + $a2 = $_.Actions + $a3 = $_.'Task Run' + + ($NamePatterns | Where-Object { $tn -match $_ }).Count -gt 0 -or + (($a1 -and (($ActionPatterns | Where-Object { $a1 -match $_ }).Count -gt 0))) -or + (($a2 -and (($ActionPatterns | Where-Object { $a2 -match $_ }).Count -gt 0))) -or + (($a3 -and (($ActionPatterns | Where-Object { $a3 -match $_ }).Count -gt 0))) + } + + return @($matches | Select-Object -ExpandProperty TaskName -Unique) + } + catch { + Write-Log -message ("EdgeTasks :: enumerate failed: {0}" -f $_.Exception.Message) -severity 'WARN' + return @() + } + } + + function Test-TaskExists([string]$TaskName) { + try { + schtasks.exe /Query /TN "$TaskName" 1>$null 2>$null + return ($LASTEXITCODE -eq 0) + } catch { + return $true + } + } + + function Remove-TaskWithRetries { + param( + [Parameter(Mandatory)][string]$TaskName + ) + + $deadline = (Get-Date).AddSeconds($PerTaskDeleteTimeoutSeconds) + $attempt = 0 + + while ((Get-Date) -lt $deadline) { + $attempt++ + + try { + schtasks.exe /Delete /TN "$TaskName" /F 2>$null | Out-Null + $exit = $LASTEXITCODE + + if ($exit -eq 0) { + if (-not (Test-TaskExists -TaskName $TaskName)) { + Write-Log -message ("EdgeTasks :: deleted {0} (attempt {1})" -f $TaskName, $attempt) -severity 'INFO' + return $true + } + Write-Log -message ("EdgeTasks :: delete reported success but task still exists: {0} (attempt {1})" -f $TaskName, $attempt) -severity 'WARN' + } else { + Write-Log -message ("EdgeTasks :: delete failed {0} (exit {1}, attempt {2})" -f $TaskName, $exit, $attempt) -severity 'WARN' + } + } + catch { + Write-Log -message ("EdgeTasks :: exception deleting {0} (attempt {1}): {2}" -f $TaskName, $attempt, $_.Exception.Message) -severity 'WARN' + } + + Start-Sleep -Seconds $PerTaskRetryIntervalSeconds + } + + Write-Log -message ("EdgeTasks :: timeout deleting {0} after {1}s" -f $TaskName, $PerTaskDeleteTimeoutSeconds) -severity 'ERROR' + return $false + } + + Write-Log -message ("EdgeTasks :: begin (timeout={0}s, interval={1}s, perTaskTimeout={2}s)" -f $TimeoutSeconds, $RetryIntervalSeconds, $PerTaskDeleteTimeoutSeconds) -severity 'DEBUG' + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $pass = 0 + + while ((Get-Date) -lt $deadline) { + $pass++ + $targets = Get-EdgeTaskNames + + if (-not $targets -or $targets.Count -eq 0) { + Write-Log -message ("EdgeTasks :: none found (pass {0})" -f $pass) -severity 'INFO' + Write-Log -message "EdgeTasks :: end (success)" -severity 'DEBUG' + return + } + + Write-Log -message ("EdgeTasks :: pass {0}: found {1} task(s)" -f $pass, $targets.Count) -severity 'INFO' + + foreach ($tn in $targets) { + $null = Remove-TaskWithRetries -TaskName $tn + } + + $stillThere = Get-EdgeTaskNames + if (-not $stillThere -or $stillThere.Count -eq 0) { + Write-Log -message ("EdgeTasks :: verification OK after pass {0}" -f $pass) -severity 'INFO' + Write-Log -message "EdgeTasks :: end (success)" -severity 'DEBUG' + return + } + + $remaining = [math]::Max(0, [int]($deadline - (Get-Date)).TotalSeconds) + Write-Log -message ("EdgeTasks :: still present after pass {0} (remaining {1}s). Sleeping {2}s..." -f $pass, $remaining, $RetryIntervalSeconds) -severity 'WARN' + Start-Sleep -Seconds $RetryIntervalSeconds + } + + $final = Get-EdgeTaskNames + if ($final -and $final.Count -gt 0) { + $sample = ($final | Select-Object -First 10) -join '; ' + Write-Log -message ("EdgeTasks :: timeout after {0}s. Remaining task(s): {1}" -f $TimeoutSeconds, $sample) -severity 'ERROR' + } else { + Write-Log -message "EdgeTasks :: end (success at timeout boundary)" -severity 'INFO' + } +} + +function Disable-SyncFromCloud { + [CmdletBinding()] + param() + + Write-Log -message "Disable-SyncFromCloud :: begin (disable Language settings sync)" -severity 'INFO' + + # 1) Always do per-user disable (no admin required) + try { + $kUser = 'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\SettingSync\Groups\Language' + New-Item -Path $kUser -Force | Out-Null + New-ItemProperty -Path $kUser -Name 'Enabled' -PropertyType DWord -Value 0 -Force | Out-Null + + $val = (Get-ItemProperty -Path $kUser -Name Enabled -ErrorAction SilentlyContinue).Enabled + Write-Log -message ("Disable-SyncFromCloud :: HKCU Language sync disabled (Enabled={0})" -f $val) -severity 'INFO' + } + catch { + Write-Log -message ("Disable-SyncFromCloud :: HKCU write failed: {0}" -f $_.Exception.Message) -severity 'WARN' + } + + # 2) If elevated, also enforce via machine policy (optional hard block) + try { + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() + ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + if ($isAdmin) { + $kPol = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\SettingSync' + New-Item -Path $kPol -Force | Out-Null + + # Common policy convention: 2 = disable + New-ItemProperty -Path $kPol -Name 'DisableLanguageSettingSync' -PropertyType DWord -Value 2 -Force | Out-Null + + $pval = (Get-ItemProperty -Path $kPol -Name DisableLanguageSettingSync -ErrorAction SilentlyContinue).DisableLanguageSettingSync + Write-Log -message ("Disable-SyncFromCloud :: HKLM policy set DisableLanguageSettingSync={0}" -f $pval) -severity 'INFO' + } + else { + Write-Log -message "Disable-SyncFromCloud :: not elevated; skipping HKLM policy enforcement" -severity 'DEBUG' + } + } + catch { + Write-Log -message ("Disable-SyncFromCloud :: HKLM policy step failed: {0}" -f $_.Exception.Message) -severity 'DEBUG' + } + + Write-Log -message "Disable-SyncFromCloud :: complete (recommend sign out/in or reboot)" -severity 'INFO' +} + +function Disable-SmartScreenStoreApps { + [CmdletBinding()] + param() + + Write-Log -message "Disable-SmartScreenStoreApps :: begin (disable SmartScreen for Microsoft Store apps)" -severity 'INFO' + + # Helper: normalize raw registry root strings to a PowerShell registry provider path + function Convert-ToRegistryProviderPath { + param([Parameter(Mandatory)][string]$Path) + + switch -Regex ($Path) { + '^HKLM:\\' { return $Path } + '^HKCU:\\' { return $Path } + '^HKEY_LOCAL_MACHINE\\' { return "Registry::$Path" } + '^HKEY_CURRENT_USER\\' { return "Registry::$Path" } + default { return $Path } + } + } + + # 1) Always do per-user disable (no admin required) + try { + $kUser = Convert-ToRegistryProviderPath 'HKCU:\Software\Microsoft\Windows\CurrentVersion\AppHost' + New-Item -Path $kUser -Force | Out-Null + + # Disable SmartScreen for Microsoft Store apps + New-ItemProperty -Path $kUser -Name 'EnableWebContentEvaluation' -PropertyType DWord -Value 0 -Force | Out-Null + + # Optional: allow override in UI (PreventOverride=0 means user can change it) + New-ItemProperty -Path $kUser -Name 'PreventOverride' -PropertyType DWord -Value 0 -Force | Out-Null + + $valEnable = (Get-ItemProperty -Path $kUser -Name EnableWebContentEvaluation -ErrorAction SilentlyContinue).EnableWebContentEvaluation + $valOverride = (Get-ItemProperty -Path $kUser -Name PreventOverride -ErrorAction SilentlyContinue).PreventOverride + + Write-Log -message ("Disable-SmartScreenStoreApps :: HKCU Store app SmartScreen disabled (EnableWebContentEvaluation={0}, PreventOverride={1})" -f $valEnable, $valOverride) -severity 'INFO' + } + catch { + Write-Log -message ("Disable-SmartScreenStoreApps :: HKCU step failed (path='{0}'): {1}" -f $kUser, $_.Exception.Message) -severity 'WARN' + } + + # 2) If elevated, also set machine-wide (optional; affects all users) + try { + $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent() + ).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + + if ($isAdmin) { + + # Ensure HKLM: drive exists (rare edge-case; makes the function more robust) + if (-not (Get-PSDrive -Name HKLM -ErrorAction SilentlyContinue)) { + New-PSDrive -Name HKLM -PSProvider Registry -Root HKEY_LOCAL_MACHINE | Out-Null + } + + $kMachine = Convert-ToRegistryProviderPath 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppHost' + + # Debug breadcrumbs in case something upstream mutates the path + Write-Log -message ("Disable-SmartScreenStoreApps :: DEBUG kMachine='{0}'" -f $kMachine) -severity 'DEBUG' + + New-Item -Path $kMachine -Force | Out-Null + New-ItemProperty -Path $kMachine -Name 'EnableWebContentEvaluation' -PropertyType DWord -Value 0 -Force | Out-Null + + $mValEnable = (Get-ItemProperty -Path $kMachine -Name EnableWebContentEvaluation -ErrorAction SilentlyContinue).EnableWebContentEvaluation + Write-Log -message ("Disable-SmartScreenStoreApps :: HKLM Store app SmartScreen disabled (EnableWebContentEvaluation={0})" -f $mValEnable) -severity 'INFO' + } + else { + Write-Log -message "Disable-SmartScreenStoreApps :: not elevated; skipping HKLM machine-wide setting" -severity 'DEBUG' + } + } + catch { + Write-Log -message ("Disable-SmartScreenStoreApps :: HKLM step failed (path='{0}'): {1}" -f $kMachine, $_.Exception.Message) -severity 'DEBUG' + } + + Write-Log -message "Disable-SmartScreenStoreApps :: complete (recommend sign out/in or restart Store apps)" -severity 'INFO' +} + + # Windows release ID. # From time to time we need to have the different releases of the same OS version $release_key = (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion') @@ -70,6 +563,23 @@ else { $os_version = $null } +$worker_location = $null +try { + $image_provisioner = Get-ItemPropertyValue -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet' -Name 'image_provisioner' -ErrorAction Stop +} catch { + Write-Log -message ("Unable to read HKLM:\SOFTWARE\Mozilla\ronin_puppet\image_provisioner: {0}" -f $_.Exception.Message) -severity 'ERROR' + exit 1 +} + +if ($image_provisioner -match '(?i)mdc1') { + $worker_location = 'MDC1 hardware' +} elseif ($image_provisioner -match '(?i)azure') { + $worker_location = 'azure vm' +} else { + Write-Log -message ("Location can't be determined (image_provisioner='{0}')" -f $image_provisioner) -severity 'ERROR' + exit 1 +} + ## Wait until explorer is set in the registry and then suppress notifications for firewall while ($true) { $explorer = Get-ItemProperty -Path "HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer" -ErrorAction SilentlyContinue @@ -95,6 +605,15 @@ switch ($os_version) { "win_11_2009" { Write-Log -Message ('{0} :: {1} - {2:o}' -f $($MyInvocation.MyCommand.Name), "Setting scrollbars to always show in task-user-init.ps1", (Get-Date).ToUniversalTime()) -severity 'DEBUG' New-ItemProperty -Path 'HKCU:\Control Panel\Accessibility' -Name 'DynamicScrollbars' -Value 0 -Force + if ($worker_location -eq 'MDC1 hardware') { + Disable-PerUserUwpServices + Remove-OneDriveScheduledTasks + Disable-OneDriveBackupPopup + Remove-EdgeScheduledTasks + ## Not currently functioning + #Disable-SyncFromCloud + #Disable-SmartScreenStoreApps + } } "win_2022" { ## Disable Server Manager Dashboard diff --git a/modules/win_scheduled_tasks/files/gw_exe_check.ps1 b/modules/win_scheduled_tasks/files/gw_exe_check.ps1 index 6f3b97cb4..c687f33d1 100644 --- a/modules/win_scheduled_tasks/files/gw_exe_check.ps1 +++ b/modules/win_scheduled_tasks/files/gw_exe_check.ps1 @@ -29,9 +29,7 @@ function Set-PXE { process { $temp_dir = "$env:SystemDrive\temp\" New-Item -ItemType Directory -Force -Path $temp_dir -ErrorAction SilentlyContinue - bcdedit /enum firmware > "$temp_dir\firmware.txt" - $fwbootmgr = Select-String -Path "$temp_dir\firmware.txt" -Pattern "{fwbootmgr}" if (!$fwbootmgr) { Write-Log -message ('{0} :: Device is configured for Legacy Boot. Exiting!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' @@ -41,7 +39,6 @@ function Set-PXE { $FullLine = ((Get-Content "$temp_dir\firmware.txt" | Select-String "IPV4|EFI Network" -Context 1 -ErrorAction Stop).Context.PreContext)[0] $GUID = '{' + $FullLine.Split('{')[1] bcdedit /set "{fwbootmgr}" bootsequence "$GUID" - Write-Log -message ('{0} :: Device will PXE boot. Restarting' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' Restart-Computer -Force Exit @@ -56,62 +53,73 @@ function Set-PXE { } } -# Main monitoring script - -# Initial sleep for 10 minutes -Write-Log -message "Sleeping 10 minutes before starting GW monitoring..." -severity 'DEBUG' -Start-Sleep -Seconds (10 * 60) - -$regPath = "HKLM:\SOFTWARE\Mozilla\Ronin" -$regName = "GW_failed" - -while ($true) { - Write-Log -message "Checking for 'generic-worker' process..." -severity 'DEBUG' - - $process = Get-Process -Name "generic-worker" -ErrorAction SilentlyContinue +function Register-FailureAndMaybePXE { + param ( + [string] $regName + ) - if (-not $process) { - Write-Log -message "Generic Worker process not found." -severity 'WARN' + $regPath = "HKLM:\SOFTWARE\Mozilla\Ronin\GW_check_failures" + if (!(Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + $currentValue = 0 + try { + $currentValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction Stop).$regName + } catch { + $currentValue = 0 + } + if ($currentValue -eq 1) { + Write-Log -message "$regName failure occurred again. Initiating PXE boot." -severity 'ERROR' + Set-PXE + exit + } else { + Write-Log -message "$regName failure detected. Rebooting system (1st failure)." -severity 'ERROR' + Set-ItemProperty -Path $regPath -Name $regName -Value 1 -Force + Restart-Computer -Force + Exit + } +} - if (!(Test-Path $regPath)) { - New-Item -Path $regPath -Force | Out-Null - } +$bootstrap_stage = (Get-ItemProperty -path "HKLM:\SOFTWARE\Mozilla\ronin_puppet").bootstrap_stage +If ($bootstrap_stage -ne 'complete') { + Write-Log -message ('{0} :: Bootstrap has not completed. EXITING!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Exit +} - $currentValue = $null - Try { - $currentValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction Stop).$regName - } Catch { - # If the property does not exist, we'll treat it as first failure - $currentValue = $null - } +# Uptime check — allow 15-minute grace period before enforcing logic +$lastBoot = (Get-CimInstance -ClassName Win32_OperatingSystem).LastBootUpTime +$uptimeMinutes = (New-TimeSpan -Start $lastBoot -End (Get-Date)).TotalMinutes +if ($uptimeMinutes -lt 15) { + Write-Log -message "System has only been up for $([math]::Round($uptimeMinutes, 1)) minutes. Skipping generic-worker check until 15-minute threshold is met." -severity 'DEBUG' + exit +} - if ($currentValue -eq 1) { - Write-Log -message "Generic Worker still missing after previous failure. Initiating PXE boot..." -severity 'ERROR' - Set-PXE - # Set-PXE will reboot and exit - } else { - Write-Log -message "First failure detected. Setting GW_failed=1." -severity 'WARN' - Set-ItemProperty -Path $regPath -Name $regName -Value 1 -Force - } - } else { - Write-Log -message "Generic Worker process is running." -severity 'DEBUG' +# Var set by the maintain system script +# Check gw_initiated env var +if ($env:gw_initiated -ne 'true') { + Write-Log -message "Environment variable gw_initiated is not true." -severity 'WARN' + Register-FailureAndMaybePXE -regName 'gw_initiated_failed' +} - if (Test-Path $regPath) { - $currentValue = $null - Try { - $currentValue = (Get-ItemProperty -Path $regPath -Name $regName -ErrorAction Stop).$regName - } Catch { - $currentValue = $null - } +# Check for generic-worker process +# Write-Log -message "Checking for 'generic-worker' process..." -severity 'DEBUG' +$process = Get-Process -Name "generic-worker" -ErrorAction SilentlyContinue +if (-not $process) { + Write-Log -message "generic-worker has not started." -severity 'WARN' + Register-FailureAndMaybePXE -regName 'process_failed' +} - if ($currentValue -eq 1) { - Write-Log -message "Generic Worker recovered. Removing GW_failed flag." -severity 'DEBUG' - Remove-ItemProperty -Path $regPath -Name $regName -Force +# Success path – clear failure flags +Write-Log -message "Generic-worker process is up and running." -severity 'DEBUG' +$regPath = "HKLM:\SOFTWARE\Mozilla\Ronin\GW_check_failures" +$failKeys = @('gw_initiated_failed', 'process_failed') +foreach ($key in $failKeys) { + if (Test-Path $regPath) { + try { + if ((Get-ItemProperty -Path $regPath -Name $key -ErrorAction Stop).$key -eq 1) { + Write-Log -message "Clearing $key failure flag." -severity 'DEBUG' + Remove-ItemProperty -Path $regPath -Name $key -Force } - } + } catch {} } - - # Sleep 5 minutes before next check - Write-Log -message "Sleeping 5 minutes until next check..." -severity 'DEBUG' - Start-Sleep -Seconds (5 * 60) } diff --git a/modules/win_scheduled_tasks/files/maintainsystem-hw.ps1 b/modules/win_scheduled_tasks/files/maintainsystem-hw.ps1 index db5803445..addc8941f 100644 --- a/modules/win_scheduled_tasks/files/maintainsystem-hw.ps1 +++ b/modules/win_scheduled_tasks/files/maintainsystem-hw.ps1 @@ -55,234 +55,56 @@ function Run-MaintainSystem { Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' } } -function Remove-OldTaskDirectories { - param ( - [string[]] $targets = @('Z:\task_*', 'C:\Users\task_*') - ) - begin { - Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } - process { - foreach ($target in ($targets | Where-Object { (Test-Path -Path ('{0}:\' -f $_[0]) -ErrorAction SilentlyContinue) })) { - $all_task_paths = @(Get-ChildItem -Path $target | Sort-Object -Property { $_.LastWriteTime }) - if ($all_task_paths.length -gt 1) { - Write-Log -message ('{0} :: {1} task directories detected matching pattern: {2}' -f $($MyInvocation.MyCommand.Name), $all_task_paths.length, $target) -severity 'INFO' - $old_task_paths = $all_task_paths[0..($all_task_paths.Length - 2)] - foreach ($old_task_path in $old_task_paths) { - try { - & takeown.exe @('/a', '/f', $old_task_path, '/r', '/d', 'Y') - & icacls.exe @($old_task_path, '/grant', 'Administrators:F', '/t') - Remove-Item -Path $old_task_path -Force -Recurse - Write-Log -message ('{0} :: removed task directory: {1}, with last write time: {2}' -f $($MyInvocation.MyCommand.Name), $old_task_path.FullName, $old_task_path.LastWriteTime) -severity 'INFO' - } - catch { - Write-Log -message ('{0} :: failed to remove task directory: {1}, with last write time: {2}. {3}' -f $($MyInvocation.MyCommand.Name), $old_task_path.FullName, $old_task_path.LastWriteTime, $_.Exception.Message) -severity 'ERROR' - } - } - } - elseif ($all_task_paths.length -eq 1) { - Write-Log -message ('{0} :: a single task directory was detected at: {1}, with last write time: {2}' -f $($MyInvocation.MyCommand.Name), $all_task_paths[0].FullName, $all_task_paths[0].LastWriteTime) -severity 'DEBUG' - } - else { - Write-Log -message ('{0} :: no task directories detected matching pattern: {1}' -f $($MyInvocation.MyCommand.Name), $target) -severity 'DEBUG' - } - } - } - end { - Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } -} -function Check-RoninNodeOptions { - param ( - [string] $inmutable = (Get-ItemProperty -path "HKLM:\SOFTWARE\Mozilla\ronin_puppet").inmutable, - [string] $flagfile = "$env:programdata\PuppetLabs\ronin\semaphore\task-claim-state.valid" - ) - begin { - Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } - process { - Write-Host $inmutable - if ($inmutable -eq 'true') { - Write-Log -message ('{0} :: Node is set to be inmutable' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Remove-Item -path $lock -ErrorAction SilentlyContinue - write-host New-item -path $flagfile - Exit-PSSession - } - } - end { - Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } -} - -Function UpdateRonin { - param ( - [string] $sourceOrg, - [string] $sourceRepo, - [string] $sourceBranch, - [string] $ronin_repo = "$env:systemdrive\ronin" - ) - begin { - Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } - process { - $sourceOrg = $(if ((Test-Path -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -ErrorAction SilentlyContinue) -and (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Organisation' -ErrorAction SilentlyContinue)) { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Organisation').Organisation } else { 'mozilla-platform-ops' }) - $sourceRepo = $(if ((Test-Path -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -ErrorAction SilentlyContinue) -and (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Repository' -ErrorAction SilentlyContinue)) { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Repository').Repository } else { 'ronin_puppet' }) - $sourceBranch = $(if ((Test-Path -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -ErrorAction SilentlyContinue) -and (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Branch' -ErrorAction SilentlyContinue)) { (Get-ItemProperty -Path 'HKLM:\SOFTWARE\Mozilla\ronin_puppet\Source' -Name 'Branch').Branch } else { 'master' }) - - Set-Location $ronin_repo - git config --global --add safe.directory "C:/ronin" - git pull https://github.com/$sourceOrg/$sourceRepo $sourceBranch - $git_exit = $LastExitCode - if ($git_exit -eq 0) { - $git_hash = (git rev-parse --verify HEAD) - Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name githash -type string -value $git_hash - Write-Log -message ('{0} :: Checking/pulling updates from https://github.com/{1}/{2}. Branch: {3}.' -f $($MyInvocation.MyCommand.Name), ($sourceOrg), ($sourceRepo), ($sourceRev)) -severity 'DEBUG' - } - else { - # Fall back to clone if pull fails - Write-Log -message ('{0} :: Git pull failed! https://github.com/{1}/{2}. Branch: {3}.' -f $($MyInvocation.MyCommand.Name), ($sourceOrg), ($sourceRepo), ($sourceRev)) -severity 'DEBUG' - Write-Log -message ('{0} :: Deleting old repository and cloning repository .' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Move-item -Path $ronin_repo\manifests\nodes.pp -Destination $env:TEMP\nodes.pp - Move-item -Path $ronin_repo\data\secrets\vault.yaml -Destination $env:TEMP\vault.yaml - #Remove-Item -Recurse -Force $ronin_repo - Start-Sleep -s 2 - git clone --single-branch --branch $sourceRev https://github.com/$sourceOrg/$sourceRepo $ronin_repo - Move-item -Path $env:TEMP\nodes.pp -Destination $ronin_repo\manifests\nodes.pp - Move-item -Path $env:TEMP\vault.yaml -Destination $ronin_repo\data\secrets\vault.yaml - } - } - end { - Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } -} -function Puppet-Run { - param ( - [int] $exit, - [string] $lock = "$env:programdata\PuppetLabs\ronin\semaphore\ronin_run.lock", - [int] $last_exit = (Get-ItemProperty "HKLM:\SOFTWARE\Mozilla\ronin_puppet").last_run_exit, - [string] $run_to_success = (Get-ItemProperty "HKLM:\SOFTWARE\Mozilla\ronin_puppet").runtosuccess, - [string] $nodes_def = "$env:systemdrive\ronin\manifests\nodes\nodes.pp", - [string] $logdir = "$env:systemdrive\logs", - [string] $fail_dir = "$env:systemdrive\fail_logs", - [string] $log_file = "$datetime-puppetrun.log", - [string] $datetime = (get-date -format yyyyMMdd-HHmm), - [string] $flagfile = "$env:programdata\PuppetLabs\ronin\semaphore\task-claim-state.valid" +function Invoke-DownloadWithRetryGithub { + Param( + [Parameter(Mandatory)] [string] $Url, + [Alias("Destination")] [string] $Path, + [string] $PAT ) - begin { - Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } - process { - - Check-RoninNodeOptions - UpdateRonin - - # Setting Env variabes for PuppetFile install and Puppet run - # The ssl variables are needed for R10k - Write-Log -message ('{0} :: Setting Puppet enviroment.' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - $env:path = "$env:programfiles\Puppet Labs\Puppet\puppet\bin;$env:programfiles\Puppet Labs\Puppet\bin;$env:path" - $env:SSL_CERT_FILE = "$env:programfiles\Puppet Labs\Puppet\puppet\ssl\cert.pem" - $env:SSL_CERT_DIR = "$env:programfiles\Puppet Labs\Puppet\puppet\ssl" - $env:FACTER_env_windows_installdir = "$env:programfiles\Puppet Labs\Puppet" - $env:HOMEPATH = "\Users\Administrator" - $env:HOMEDRIVE = "C:" - $env:PL_BASEDIR = "$env:programfiles\Puppet Labs\Puppet" - $env:PUPPET_DIR = "$env:programfiles\Puppet Labs\Puppet" - $env:RUBYLIB = "$env:programfiles\Puppet Labs\Puppet\lib" - $env:USERNAME = "Administrator" - $env:USERPROFILE = "$env:systemdrive\Users\Administrator" - - # This is temporary and should be removed after the cloud_windows branch is merged - # Hiera lookups will fail after the merge if this is not in place following the merge - <# - if((test-path $env:systemdrive\ronin\win_hiera.yaml)) { - $hiera = "win_hiera.yaml" - } else { - $hiera = "hiera.yaml" - } - #> - # this will break Win 10 1803 if this is merged into the master brnach - $hiera = "hiera.yaml" - - # Needs to be removed from path or a wrong puppet file will be used - $env:path = ($env:path.Split(';') | Where-Object { $_ -ne "$env:programfiles\Puppet Labs\Puppet\puppet\bin" }) -join ';' - If (!(test-path $fail_dir)) { - New-Item -ItemType Directory -Force -Path $fail_dir - } - Get-ChildItem -Path $logdir\*.log -Recurse | Move-Item -Destination $logdir\old -ErrorAction SilentlyContinue - Write-Log -message ('{0} :: Initiating Puppet apply .' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - puppet apply manifests\nodes.pp --onetime --verbose --no-daemonize --no-usecacheonfailure --detailed-exitcodes --no-splay --show_diff --modulepath=modules`;r10k_modules --hiera_config=$hiera --logdest $logdir\$log_file - [int]$puppet_exit = $LastExitCode - - if ($run_to_success -eq 'true') { - if (($puppet_exit -ne 0) -and ($puppet_exit -ne 2)) { - if ($last_exit -eq 0) { - Write-Log -message ('{0} :: Puppet apply failed.' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $puppet_exit - Remove-Item $lock -ErrorAction SilentlyContinue - # If the Puppet run fails send logs to papertrail - # Nxlog watches $fail_dir for files names *-puppetrun.log - Move-Item $logdir\$log_file -Destination $fail_dir - shutdown @('-r', '-t', '0', '-c', 'Reboot; Puppet apply failed', '-f', '-d', '4:5') - } - elseif ($last_exit -ne 0) { - Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $puppet_exit - Remove-Item $lock - Move-Item $logdir\$log_file -Destination $fail_dir - Write-Log -message ('{0} :: Puppet apply failed. Waiting 10 minutes beofre Reboot' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Start-Sleep 600 - shutdown @('-r', '-t', '0', '-c', 'Reboot; Puppet apply failed', '-f', '-d', '4:5') - } - } - elseif (($puppet_exit -match 0) -or ($puppet_exit -match 2)) { - Write-Log -message ('{0} :: Puppet apply successful' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $puppet_exit - Remove-Item -path $lock - New-item -path $flagfile + if (-not $Path) { + $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join '' + $re = "[{0}]" -f [RegEx]::Escape($invalidChars) + $fileName = [IO.Path]::GetFileName($Url) -replace $re + if ([String]::IsNullOrEmpty($fileName)) { $fileName = [System.IO.Path]::GetRandomFileName() } + $Path = Join-Path -Path "${env:Temp}" -ChildPath $fileName + } + Write-Host "Downloading package from $Url to $Path..." + $interval = 30 + $downloadStartTime = Get-Date + for ($retries = 20; $retries -gt 0; $retries--) { + try { + $attemptStartTime = Get-Date + $Headers = @{ + Accept = "application/vnd.github+json" + Authorization = "Bearer $($PAT)" + "X-GitHub-Api-Version" = "2022-11-28" } - else { - Write-Log -message ('{0} :: Unable to detrimine state post Puppet apply' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Set-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet -name last_exit -type dword -value $last_exit - Move-Item $logdir\$log_file -Destination $fail_dir - Remove-Item -path $lock - shutdown @('-r', '-t', '600', '-c', 'Reboot; Unveriable state', '-f', '-d', '4:5') + $response = Invoke-WebRequest -Uri $Url -Headers $Headers -OutFile $Path + $attemptSeconds = [math]::Round(($(Get-Date) - $attemptStartTime).TotalSeconds, 2) + Write-Host "Package downloaded in $attemptSeconds seconds" + Write-Host "Status: $($response.statuscode)" + break + } catch { + $attemptSeconds = [math]::Round(($(Get-Date) - $attemptStartTime).TotalSeconds, 2) + Write-Warning "Package download failed in $attemptSeconds seconds" + Write-Host "Status: $($response.statuscode)" + Write-Warning $_.Exception.Message + if ($_.Exception.InnerException.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { + Write-Warning "Request returned 404 Not Found. Aborting download." + $retries = 0 } } - } - end { - Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } -} - -function StartWorkerRunner { - param ( - ) - begin { - Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' - } - process { - ## Checking for issues with the user profile. - $lastBootTime = Get-WinEvent -LogName "System" -FilterXPath "" | - Select-Object -First 1 | - ForEach-Object { $_.TimeCreated } - $eventIDs = @(1511, 1515) - - $events = Get-WinEvent -LogName "Application" | - Where-Object { $_.ID -in $eventIDs -and $_.TimeCreated -gt $lastBootTime } | - Sort-Object TimeCreated -Descending | Select-Object -First 1 - - if ($events) { - Write-Log -message ('{0} :: Possible User Profile Corruption. Restarting' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Restart-Computer -Force - exit + if ($retries -eq 0) { + $totalSeconds = [math]::Round(($(Get-Date) - $downloadStartTime).TotalSeconds, 2) + throw "Package download failed after $totalSeconds seconds" } - Start-Service -Name worker-runner - } - end { - Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + Write-Warning "Waiting $interval seconds before retrying (retries left: $retries)..." + Start-Sleep -Seconds $interval } + return $Path } -function CompareConfig { +function CompareConfigBasic { param ( [string]$yaml_url = "https://raw.githubusercontent.com/mozilla-platform-ops/worker-images/refs/heads/main/provisioners/windows/MDC1Windows/pools.yml", [string]$PAT @@ -294,227 +116,240 @@ function CompareConfig { process { - $yaml = $null - $SETPXE = $false - + $yaml = $null + $yamlHash = $null $IPAddress = $null - $Ethernet = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | Where-Object { $_.Name -match "ethernet" } + + # === Retrieve IP address === + $Ethernet = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | + Where-Object { $_.Name -match "ethernet" } try { $IPAddress = ($Ethernet.GetIPProperties().UnicastAddresses | Where-Object { $_.Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.Address.IPAddressToString -ne "127.0.0.1" } | Select-Object -First 1 -ExpandProperty Address).IPAddressToString - - if (-not $IPAddress) { - throw "No IP address found using .NET method." - } } catch { try { $NetshOutput = netsh interface ip show addresses $IPAddress = ($NetshOutput -match "IP Address" | ForEach-Object { - if ($_ -notmatch "127.0.0.1") { - $_ -replace ".*?:\s*", "" - } + if ($_ -notmatch "127.0.0.1") { $_ -replace ".*?:\s*", "" } })[0] } catch { - Write-Log -message "Failed to get IP address" -severity 'ERROR' + Write-Log -message ('{0} :: Failed to get IP address' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' } } - if ($IPAddress) { - Write-Log -message "IP Address: $IPAddress" -severity 'INFO' - } - else { - Write-Log -message "No IP Address could be determined." -severity 'ERROR' + if (-not $IPAddress) { + Write-Log -message ('{0} :: No IP Address could be determined.' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Restart-Computer -Force return } + Write-Log -message ('{0} :: IP Address: {1}' -f $MyInvocation.MyCommand.Name, $IPAddress) -severity 'INFO' + + + # === Resolve DNS to get worker name === try { $ResolvedName = (Resolve-DnsName -Name $IPAddress -Server "10.48.75.120").NameHost } catch { - Write-Log -message "DNS resolution failed." -severity 'ERROR' + Write-Log -message ('{0} :: DNS resolution failed' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Restart-Computer -Force return } - Write-Log -message "Resolved Name: $ResolvedName" -severity 'INFO' + Write-Log -message ('{0} :: Resolved Name: {1}' -f $MyInvocation.MyCommand.Name, $ResolvedName) -severity 'INFO' $index = $ResolvedName.IndexOf('.') if ($index -lt 0) { - Write-Log -message "Invalid hostname format." -severity 'ERROR' + Write-Log -message ('{0} :: Invalid hostname format.' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Restart-Computer -Force return } $worker_node_name = $ResolvedName.Substring(0, $index) - $domain_suffix = $ResolvedName.Substring($index + 1) + Write-Log -message ('{0} :: Host name set to: {1}' -f $MyInvocation.MyCommand.Name, $worker_node_name) -severity 'INFO' - Write-Log -message "Host name set to: $worker_node_name" -severity 'INFO' + # === Load local ronin puppet values === $localHash = (Get-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet).GITHASH $localPool = (Get-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet).worker_pool_id - $maxRetries = 5 - $retryDelay = 10 - $attempt = 0 - $success = $false - while ($attempt -lt $maxRetries -and -not $success) { - try { - $Headers = @{ - Accept = "application/vnd.github+json" - Authorization = "Bearer $($PAT)" - "X-GitHub-Api-Version" = "2022-11-28" - } - $response = Invoke-WebRequest -Uri $yaml_url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop -Headers $Headers - $yaml = $response.Content | ConvertFrom-Yaml - if ($yaml) { - $success = $true - } - else { - throw "YAML content is empty" - Write-Log -message "YAML content is empty" -severity 'WARN' - } - } - catch { - Write-Log -message "Attempt $($attempt + 1): Failed to fetch YAML - $_" -severity 'WARN' - Start-Sleep -Seconds $retryDelay - $attempt++ - } + # === Load PAT for YAML download === + $patFile = "D:\Secrets\pat.txt" + if (-not (Test-Path $patFile)) { + Write-Log -message ('{0} :: PAT file missing: {1}' -f $MyInvocation.MyCommand.Name, $patFile) -severity 'ERROR' + Set-PXE + Restart-Computer -Force + return + } + + $PAT = Get-Content $patFile -ErrorAction Stop + + + # === Download YAML using unified retry function === + $tempYamlPath = "$env:TEMP\pools.yml" + + $splat = @{ + Url = $yaml_url + Path = $tempYamlPath + PAT = $PAT } - if (-not $success) { - Write-Log -message "YAML could not be loaded after $maxRetries attempts." -severity 'ERROR' + if (-not (Invoke-DownloadWithRetryGithub @splat)) { + Write-Log -message ('{0} :: YAML download failed after retries. PXE rebooting.' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Set-PXE + Restart-Computer -Force return } + + # === Parse YAML === + try { + $yaml = Get-Content $tempYamlPath -Raw | ConvertFrom-Yaml + } + catch { + Write-Log -message ('{0} :: YAML parsing failed: {1}' -f $MyInvocation.MyCommand.Name, $_) -severity 'ERROR' + Set-PXE + Restart-Computer -Force + return + } + + + # === Lookup this worker in pools.yml === $found = $false foreach ($pool in $yaml.pools) { - foreach ($node in $pool.nodes) { - if ($node -eq $worker_node_name) { - $WorkerPool = $pool.name - $yamlHash = $pool.hash - $yamlImageName = $pool.image - $yamlImageDir = "D:\" + $yamlImageName - $found = $true - break - } + if ($pool.nodes -contains $worker_node_name) { + $WorkerPool = $pool.name + $yamlHash = $pool.hash + $yamlImageName = $pool.image + $yamlImageDir = "D:\" + $yamlImageName + $found = $true + break } - if ($found) { break } } if (-not $found) { - Write-Log -message "Node name not found in YAML!!" -severity 'ERROR' - exit 96 + Write-Log -message ('{0} :: Node not found in YAML. PXE rebooting.' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Set-PXE + Restart-Computer -Force + return } - Write-Log -message "=== Configuration Comparison ===" -severity 'INFO' + Write-Log -message ('{0} :: === Configuration Comparison ===' -f $MyInvocation.MyCommand.Name) -severity 'INFO' - if ($localPool -eq $WorkerPool) { - Write-Log -message "Worker Pool Match: $WorkerPool" -severity 'INFO' + + # === Compare pool === + if ($localPool -ne $WorkerPool) { + Write-Log -message ('{0} :: Worker Pool MISMATCH!' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + $SETPXE = $true } else { - Write-Log -message "Worker Pool MISMATCH!" -severity 'ERROR' - $SETPXE = $true - Start-Sleep -s 1 + Write-Log -message ('{0} :: Worker Pool Match: {1}' -f $MyInvocation.MyCommand.Name, $WorkerPool) -severity 'INFO' } - if ($localHash -eq $yamlHash) { - Write-Log -message "Git Hash Match: $yamlHash" -severity 'INFO' + + # === Compare puppet githash === + if ([string]::IsNullOrWhiteSpace($yamlHash) -or $localHash -ne $yamlHash) { + Write-Log -message ('{0} :: Git Hash MISMATCH or missing YAML hash!' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Write-Log -message ('{0} :: Local: {1}' -f $MyInvocation.MyCommand.Name, $localHash) -severity 'WARN' + Write-Log -message ('{0} :: YAML : {1}' -f $MyInvocation.MyCommand.Name, $yamlHash) -severity 'WARN' + $SETPXE = $true } else { - Write-Log -message "Git Hash MISMATCH!" -severity 'ERROR' - Write-Log -message "Local: $localHash" -severity 'WARN' - Write-Log -message "YAML : $yamlHash" -severity 'WARN' - $SETPXE = $true - Start-Sleep -s 1 + Write-Log -message ('{0} :: Git Hash Match: {1}' -f $MyInvocation.MyCommand.Name, $yamlHash) -severity 'INFO' } + # === Verify local puppet image directory exists === if (!(Test-Path $yamlImageDir)) { - Write-Log -message "Image Directory MISMATCH!" -severity 'ERROR' - Write-Log -message "YAML : $yamlImageDir NOT FOUND" -severity 'WARN' + Write-Log -message ('{0} :: Image directory missing: {1}' -f $MyInvocation.MyCommand.Name, $yamlImageDir) -severity 'ERROR' $SETPXE = $true - Start-Sleep -s 1 } + + + # === If anything mismatched, PXE reboot === if ($SETPXE) { - Write-Log -message "Configuration MISMATCH! Initiating self re-deploy!" -severity 'ERROR' + Write-Log -message ('{0} :: Configuration mismatch — initiating PXE + reboot.' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' Set-PXE + Restart-Computer -Force + return } - Write-Log -message "SETPXE set to: $SETPXE" -severity 'DEBUG' - } + Write-Log -message ('{0} :: Configuration is correct. No reboot required.' -f $MyInvocation.MyCommand.Name) -severity 'INFO' + } end { Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' } } +function Remove-OldTaskDirectories { + param ( + [string[]] $targets = @('Z:\task_*', 'C:\Users\task_*') + ) + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } + process { + foreach ($target in ($targets | Where-Object { (Test-Path -Path ('{0}:\' -f $_[0]) -ErrorAction SilentlyContinue) })) { + $all_task_paths = @(Get-ChildItem -Path $target | Sort-Object -Property { $_.LastWriteTime }) + if ($all_task_paths.length -gt 1) { + Write-Log -message ('{0} :: {1} task directories detected matching pattern: {2}' -f $($MyInvocation.MyCommand.Name), $all_task_paths.length, $target) -severity 'INFO' + $old_task_paths = $all_task_paths[0..($all_task_paths.Length - 2)] + foreach ($old_task_path in $old_task_paths) { + try { + & takeown.exe @('/a', '/f', $old_task_path, '/r', '/d', 'Y') + & icacls.exe @($old_task_path, '/grant', 'Administrators:F', '/t') + Remove-Item -Path $old_task_path -Force -Recurse + Write-Log -message ('{0} :: removed task directory: {1}, with last write time: {2}' -f $($MyInvocation.MyCommand.Name), $old_task_path.FullName, $old_task_path.LastWriteTime) -severity 'INFO' + } + catch { + Write-Log -message ('{0} :: failed to remove task directory: {1}, with last write time: {2}. {3}' -f $($MyInvocation.MyCommand.Name), $old_task_path.FullName, $old_task_path.LastWriteTime, $_.Exception.Message) -severity 'ERROR' + } + } + } + elseif ($all_task_paths.length -eq 1) { + Write-Log -message ('{0} :: a single task directory was detected at: {1}, with last write time: {2}' -f $($MyInvocation.MyCommand.Name), $all_task_paths[0].FullName, $all_task_paths[0].LastWriteTime) -severity 'DEBUG' + } + else { + Write-Log -message ('{0} :: no task directories detected matching pattern: {1}' -f $($MyInvocation.MyCommand.Name), $target) -severity 'DEBUG' + } + } + } + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} -function StartGenericWorker { +function StartWorkerRunner { param ( - [string] $GW_dir = "$env:systemdrive\generic-worker" ) begin { Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' } process { - # Check for user profile issues + ## Checking for issues with the user profile. $lastBootTime = Get-WinEvent -LogName "System" -FilterXPath "" | Select-Object -First 1 | ForEach-Object { $_.TimeCreated } - $eventIDs = @(1511, 1515) + $events = Get-WinEvent -LogName "Application" | Where-Object { $_.ID -in $eventIDs -and $_.TimeCreated -gt $lastBootTime } | Sort-Object TimeCreated -Descending | Select-Object -First 1 if ($events) { Write-Log -message ('{0} :: Possible User Profile Corruption. Restarting' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - Start-Sleep -Seconds 5 Restart-Computer -Force exit } - - Set-Location -Path $GW_dir - - & $GW_dir\generic-worker.exe run --config generic-worker.config 2>&1 | Out-File -FilePath generic-worker.log -Encoding utf8 - $exitCode = $LASTEXITCODE - - Write-Log -message ('{0} :: GW exited with code {1}' -f $($MyInvocation.MyCommand.Name), $exitCode) -severity 'DEBUG' - - switch ($exitCode) { - 68 { - - Write-Log -message ('{0} :: Idle timeout detected (exit code 68). Checking for latest Config.' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - CompareConfig -PAT (Get-Content "D:\Secrets\pat.txt") - - Write-Log -message ('{0} :: Copying current-task-user.json to next-task-user.json' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - $src = Join-Path $GW_dir 'current-task-user.json' - $dest = Join-Path $GW_dir 'next-task-user.json' - if (Test-Path $src) { - Copy-Item -Path $src -Destination $dest -Force - Write-Log -message ('{0} :: File copied successfully' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' - } - else { - Write-Log -message ('{0} :: Source file not found: {1}' -f $($MyInvocation.MyCommand.Name), $src) -severity 'WARNING' - } - - ## Restart Explorer/close out exisiting windows - Stop-Process -Name explorer -Force - - Start-Sleep -s 2 - StartGenericWorker - return - } - default { - Write-Log -message ('{0} :: Non-idle exit code {1}. Rebooting' -f $($MyInvocation.MyCommand.Name), $exitCode) -severity 'DEBUG' - Start-Sleep -Seconds 1 - Restart-Computer -Force - } - } + Start-Service -Name worker-runner + [Environment]::SetEnvironmentVariable('gw_initiated', 'true', 'Machine') } end { Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' @@ -606,7 +441,6 @@ function Set-PXE { Write-Log -message ('{0} :: Device will PXE boot. Restarting' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' Restart-Computer -Force - Exit } Catch { Write-Log -message ('{0} :: Unable to set next boot to PXE. Exiting!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' @@ -656,16 +490,8 @@ if ($refresh_rate -ne "60") { $bootstrap_stage = (Get-ItemProperty -path "HKLM:\SOFTWARE\Mozilla\ronin_puppet").bootstrap_stage If ($bootstrap_stage -eq 'complete') { - - $tasks = Get-ScheduledTask | Where-Object { $_.TaskName -eq "bootstrap" } - - if ($tasks) { - $tasks | ForEach-Object { - Stop-ScheduledTask -TaskName $task.TaskName -TaskPath $task.TaskPath - Unregister-ScheduledTask -TaskName $_.TaskName -TaskPath $_.TaskPath -Confirm:$false - Write-Host "Deleted task '$($_.TaskName)' at path '$($_.TaskPath)'." - } - } + CompareConfigBasic + Start-Sleep -Seconds 2 Run-MaintainSystem ## We're getting user profile corruption errors, so let's check that the user is logged in using quser.exe for ($i = 0; $i -lt 3; $i++) { @@ -679,7 +505,6 @@ If ($bootstrap_stage -eq 'complete') { break } } - ## Let's make sure the machine is online before checking the internet Test-ConnectionUntilOnline @@ -687,12 +512,8 @@ If ($bootstrap_stage -eq 'complete') { ## Instead of querying chocolatey each time this runs, let's query chrome json endoint and check locally installed version Get-LatestGoogleChrome - $processname = "StartMenuExperienceHost" - if ($null -ne $process) { - Stop-Process -Name $processname -force - } - CompareConfig -PAT (Get-Content "D:\Secrets\pat.txt") - StartGenericWorker + StartWorkerRunner + Exit-PSSession } else { Write-Log -message ('{0} :: Bootstrap has not completed. EXITING!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' diff --git a/modules/win_scheduled_tasks/files/onedrive_task_deletion.ps1 b/modules/win_scheduled_tasks/files/onedrive_task_deletion.ps1 new file mode 100644 index 000000000..2350f14e0 --- /dev/null +++ b/modules/win_scheduled_tasks/files/onedrive_task_deletion.ps1 @@ -0,0 +1,167 @@ +<# +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +#> + +function Write-Log { + param ( + [string] $message, + [string] $severity = 'INFO', + [string] $source = 'MaintainSystem', + [string] $logName = 'Application' + ) + if (!([Diagnostics.EventLog]::Exists($logName)) -or !([Diagnostics.EventLog]::SourceExists($source))) { + New-EventLog -LogName $logName -Source $source + } + switch ($severity) { + 'DEBUG' { + $entryType = 'SuccessAudit' + $eventId = 2 + break + } + 'WARN' { + $entryType = 'Warning' + $eventId = 3 + break + } + 'ERROR' { + $entryType = 'Error' + $eventId = 4 + break + } + default { + $entryType = 'Information' + $eventId = 1 + break + } + } + Write-EventLog -LogName $logName -Source $source -EntryType $entryType -Category 0 -EventID $eventId -Message $message + if ([Environment]::UserInteractive) { + $fc = @{ 'Information' = 'White'; 'Error' = 'Red'; 'Warning' = 'DarkYellow'; 'SuccessAudit' = 'DarkGray' }[$entryType] + Write-Host -object $message -ForegroundColor $fc + } +} + +function Remove-OneDriveScheduledTasks { + [CmdletBinding()] + param( + [int]$TimeoutSeconds = 180, + [int]$RetryIntervalSeconds = 10, + [int]$PerTaskDeleteTimeoutSeconds = 60, + [int]$PerTaskRetryIntervalSeconds = 3 + ) + + function Get-OneDriveTaskNames { + try { + $rows = @(schtasks.exe /Query /FO CSV /V 2>$null | ConvertFrom-Csv) + if (-not $rows -or $rows.Count -eq 0) { return @() } + + $matches = $rows | Where-Object { + ($_.TaskName -match '(?i)onedrive') -or + (($_.'Task To Run') -and (($_.'Task To Run') -match '(?i)onedrive(\\.exe)?')) -or + (($_.Actions) -and ($_.Actions -match '(?i)onedrive(\\.exe)?')) -or + (($_.'Task Run') -and (($_.'Task Run') -match '(?i)onedrive(\\.exe)?')) -or + (($_.Actions) -and ($_.Actions -match '(?i)OneDriveSetup\.exe|\\OneDrive\.exe')) -or + (($_.'Task To Run') -and (($_.'Task To Run') -match '(?i)OneDriveSetup\.exe|\\OneDrive\.exe')) + } + + return @($matches | Select-Object -ExpandProperty TaskName -Unique) + } + catch { + Write-Log -message ("OneDriveTasks :: enumerate failed: {0}" -f $_.Exception.Message) -severity 'WARN' + return @() + } + } + + function Test-TaskExists([string]$TaskName) { + try { + schtasks.exe /Query /TN "$TaskName" 1>$null 2>$null + return ($LASTEXITCODE -eq 0) + } catch { + return $true # assume it exists if we couldn't query + } + } + + function Remove-TaskWithRetries { + param( + [Parameter(Mandatory)][string]$TaskName + ) + + $deadline = (Get-Date).AddSeconds($PerTaskDeleteTimeoutSeconds) + $attempt = 0 + + while ((Get-Date) -lt $deadline) { + $attempt++ + + try { + schtasks.exe /Delete /TN "$TaskName" /F 2>$null | Out-Null + $exit = $LASTEXITCODE + + if ($exit -eq 0) { + # Some tasks "delete" but linger briefly; verify + if (-not (Test-TaskExists -TaskName $TaskName)) { + Write-Log -message ("OneDriveTasks :: deleted {0} (attempt {1})" -f $TaskName, $attempt) -severity 'INFO' + return $true + } + + Write-Log -message ("OneDriveTasks :: delete reported success but task still exists: {0} (attempt {1})" -f $TaskName, $attempt) -severity 'WARN' + } else { + Write-Log -message ("OneDriveTasks :: delete failed {0} (exit {1}, attempt {2})" -f $TaskName, $exit, $attempt) -severity 'WARN' + } + } + catch { + Write-Log -message ("OneDriveTasks :: exception deleting {0} (attempt {1}): {2}" -f $TaskName, $attempt, $_.Exception.Message) -severity 'WARN' + } + + Start-Sleep -Seconds $PerTaskRetryIntervalSeconds + } + + Write-Log -message ("OneDriveTasks :: timeout deleting {0} after {1}s" -f $TaskName, $PerTaskDeleteTimeoutSeconds) -severity 'ERROR' + return $false + } + + Write-Log -message ("OneDriveTasks :: begin (timeout={0}s, interval={1}s, perTaskTimeout={2}s)" -f $TimeoutSeconds, $RetryIntervalSeconds, $PerTaskDeleteTimeoutSeconds) -severity 'DEBUG' + + $deadline = (Get-Date).AddSeconds($TimeoutSeconds) + $pass = 0 + + while ((Get-Date) -lt $deadline) { + $pass++ + $targets = Get-OneDriveTaskNames + + if (-not $targets -or $targets.Count -eq 0) { + Write-Log -message ("OneDriveTasks :: none found (pass {0})" -f $pass) -severity 'INFO' + Write-Log -message "OneDriveTasks :: end (success)" -severity 'DEBUG' + return + } + + Write-Log -message ("OneDriveTasks :: pass {0}: found {1} task(s)" -f $pass, $targets.Count) -severity 'INFO' + + foreach ($tn in $targets) { + $null = Remove-TaskWithRetries -TaskName $tn + } + + # Re-check right away; if still present, sleep and retry until overall timeout + $stillThere = Get-OneDriveTaskNames + if (-not $stillThere -or $stillThere.Count -eq 0) { + Write-Log -message ("OneDriveTasks :: verification OK after pass {0}" -f $pass) -severity 'INFO' + Write-Log -message "OneDriveTasks :: end (success)" -severity 'DEBUG' + return + } + + $remaining = [math]::Max(0, [int]($deadline - (Get-Date)).TotalSeconds) + Write-Log -message ("OneDriveTasks :: still present after pass {0} (remaining {1}s). Sleeping {2}s..." -f $pass, $remaining, $RetryIntervalSeconds) -severity 'WARN' + Start-Sleep -Seconds $RetryIntervalSeconds + } + + $final = Get-OneDriveTaskNames + if ($final -and $final.Count -gt 0) { + $sample = ($final | Select-Object -First 10) -join '; ' + Write-Log -message ("OneDriveTasks :: timeout after {0}s. Remaining task(s): {1}" -f $TimeoutSeconds, $sample) -severity 'ERROR' + } else { + Write-Log -message "OneDriveTasks :: end (success at timeout boundary)" -severity 'INFO' + } +} + +Remove-OneDriveScheduledTasks diff --git a/modules/win_scheduled_tasks/files/self_redeploy_check.ps1 b/modules/win_scheduled_tasks/files/self_redeploy_check.ps1 new file mode 100644 index 000000000..cd7172719 --- /dev/null +++ b/modules/win_scheduled_tasks/files/self_redeploy_check.ps1 @@ -0,0 +1,359 @@ +<# +This Source Code Form is subject to the terms of the Mozilla Public +License, v. 2.0. If a copy of the MPL was not distributed with this +file, You can obtain one at http://mozilla.org/MPL/2.0/. +#> + +function Write-Log { + param ( + [string] $message, + [string] $severity = 'INFO', + [string] $source = 'MaintainSystem', + [string] $logName = 'Application' + ) + if (!([Diagnostics.EventLog]::Exists($logName)) -or !([Diagnostics.EventLog]::SourceExists($source))) { + New-EventLog -LogName $logName -Source $source + } + switch ($severity) { + 'DEBUG' { + $entryType = 'SuccessAudit' + $eventId = 2 + break + } + 'WARN' { + $entryType = 'Warning' + $eventId = 3 + break + } + 'ERROR' { + $entryType = 'Error' + $eventId = 4 + break + } + default { + $entryType = 'Information' + $eventId = 1 + break + } + } + Write-EventLog -LogName $logName -Source $source -EntryType $entryType -Category 0 -EventID $eventId -Message $message + if ([Environment]::UserInteractive) { + $fc = @{ 'Information' = 'White'; 'Error' = 'Red'; 'Warning' = 'DarkYellow'; 'SuccessAudit' = 'DarkGray' }[$entryType] + Write-Host -object $message -ForegroundColor $fc + } +} +function Invoke-DownloadWithRetryGithub { + Param( + [Parameter(Mandatory)] [string] $Url, + [Alias("Destination")] [string] $Path, + [string] $PAT + ) + if (-not $Path) { + $invalidChars = [IO.Path]::GetInvalidFileNameChars() -join '' + $re = "[{0}]" -f [RegEx]::Escape($invalidChars) + $fileName = [IO.Path]::GetFileName($Url) -replace $re + if ([String]::IsNullOrEmpty($fileName)) { $fileName = [System.IO.Path]::GetRandomFileName() } + $Path = Join-Path -Path "${env:Temp}" -ChildPath $fileName + } + Write-Host "Downloading package from $Url to $Path..." + $interval = 30 + $downloadStartTime = Get-Date + for ($retries = 20; $retries -gt 0; $retries--) { + try { + $attemptStartTime = Get-Date + $Headers = @{ + Accept = "application/vnd.github+json" + Authorization = "Bearer $($PAT)" + "X-GitHub-Api-Version" = "2022-11-28" + } + $response = Invoke-WebRequest -Uri $Url -Headers $Headers -OutFile $Path + $attemptSeconds = [math]::Round(($(Get-Date) - $attemptStartTime).TotalSeconds, 2) + Write-Host "Package downloaded in $attemptSeconds seconds" + Write-Host "Status: $($response.statuscode)" + break + } catch { + $attemptSeconds = [math]::Round(($(Get-Date) - $attemptStartTime).TotalSeconds, 2) + Write-Warning "Package download failed in $attemptSeconds seconds" + Write-Host "Status: $($response.statuscode)" + Write-Warning $_.Exception.Message + if ($_.Exception.InnerException.Response.StatusCode -eq [System.Net.HttpStatusCode]::NotFound) { + Write-Warning "Request returned 404 Not Found. Aborting download." + $retries = 0 + } + } + if ($retries -eq 0) { + $totalSeconds = [math]::Round(($(Get-Date) - $downloadStartTime).TotalSeconds, 2) + throw "Package download failed after $totalSeconds seconds" + } + Write-Warning "Waiting $interval seconds before retrying (retries left: $retries)..." + Start-Sleep -Seconds $interval + } + return $Path +} + +# ------------------------------------------------------------------- +# Persistent PXE Pending Flag +# ------------------------------------------------------------------- +function Set-PXEPendingFlag { + param([switch]$Clear) + + $regPath = "HKLM:\SOFTWARE\Mozilla\PXE" + $name = "PendingPXE" + + if ($Clear) { + if (Test-Path $regPath) { + Remove-ItemProperty -Path $regPath -Name $name -ErrorAction SilentlyContinue + } + return + } + + if (-not (Test-Path $regPath)) { + New-Item -Path $regPath -Force | Out-Null + } + + New-ItemProperty -Path $regPath -Name $name -Value "1" -PropertyType String -Force | Out-Null +} + +function Get-PXEPendingFlag { + $regPath = "HKLM:\SOFTWARE\Mozilla\PXE" + $name = "PendingPXE" + + if (Test-Path "$regPath\$name") { return $true } + return $false +} + +function CompareConfig { + param ( + [string]$yaml_url = "https://raw.githubusercontent.com/mozilla-platform-ops/worker-images/refs/heads/main/provisioners/windows/MDC1Windows/pools.yml", + [string]$PAT + ) + + begin { + Write-Log -message ('{0} :: begin - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + + # Detect previous deferral due to active task + if (Get-PXEPendingFlag) { + Write-Log -message "PXE/Reboot pending task completion from previous run." -severity 'INFO' + } + } + + process { + $yaml = $null + $SETPXE = $false + $yamlHash = $null + $IPAddress = $null + + # ------------------------------- + # Resolve IP + # ------------------------------- + $Ethernet = [System.Net.NetworkInformation.NetworkInterface]::GetAllNetworkInterfaces() | + Where-Object { $_.Name -match "ethernet" } + + try { + $IPAddress = ($Ethernet.GetIPProperties().UnicastAddresses | + Where-Object { $_.Address.AddressFamily -eq [System.Net.Sockets.AddressFamily]::InterNetwork -and $_.Address.IPAddressToString -ne "127.0.0.1" } | + Select-Object -First 1 -ExpandProperty Address).IPAddressToString + } + catch { + try { + $NetshOutput = netsh interface ip show addresses + $IPAddress = ($NetshOutput -match "IP Address" | ForEach-Object { + if ($_ -notmatch "127.0.0.1") { $_ -replace ".*?:\s*", "" } + })[0] + } + catch { + Write-Log -message "Failed to get IP address" -severity 'ERROR' + } + } + + if ($IPAddress) { + Write-Log -message "IP Address: $IPAddress" -severity 'INFO' + } + else { + Write-Log -message "No IP Address could be determined." -severity 'ERROR' + return + } + + try { + $ResolvedName = (Resolve-DnsName -Name $IPAddress -Server "10.48.75.120").NameHost + } + catch { + Write-Log -message "DNS resolution failed." -severity 'ERROR' + return + } + + Write-Log -message "Resolved Name: $ResolvedName" -severity 'INFO' + + $index = $ResolvedName.IndexOf('.') + if ($index -lt 0) { + Write-Log -message "Invalid hostname format." -severity 'ERROR' + return + } + + $worker_node_name = $ResolvedName.Substring(0, $index) + Write-Log -message "Host name set to: $worker_node_name" -severity 'INFO' + + $localHash = (Get-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet).GITHASH + $localPool = (Get-ItemProperty -Path HKLM:\SOFTWARE\Mozilla\ronin_puppet).worker_pool_id + + $patFile = "D:\Secrets\pat.txt" + if (-not (Test-Path $patFile)) { + Write-Log -message ('{0} :: PAT file missing: {1}' -f $MyInvocation.MyCommand.Name, $patFile) -severity 'ERROR' + Set-PXE + Set-PXEPendingFlag -Clear + Restart-Computer -Force + return + } + + $tempYamlPath = "$env:TEMP\pools.yml" + $PAT = Get-Content $patFile -ErrorAction Stop + + $splat = @{ + Url = $yaml_url + Path = $tempYamlPath + PAT = $PAT + } + + if (-not (Invoke-DownloadWithRetryGithub @splat)) { + Write-Log -message ('{0} :: YAML download failed after retries. PXE rebooting.' -f $MyInvocation.MyCommand.Name) -severity 'ERROR' + Set-PXE + Set-PXEPendingFlag -Clear + Restart-Computer -Force + return + } + + try { + $yaml = Get-Content $tempYamlPath -Raw | ConvertFrom-Yaml + } + catch { + Write-Log -message ('{0} :: YAML parsing failed: {1}' -f $MyInvocation.MyCommand.Name, $_) -severity 'ERROR' + Set-PXE + Set-PXEPendingFlag -Clear + Restart-Computer -Force + return + } + + $found = $false + if ($yaml) { + foreach ($pool in $yaml.pools) { + foreach ($node in $pool.nodes) { + if ($node -eq $worker_node_name) { + $WorkerPool = $pool.name + $yamlHash = $pool.hash + $yamlImageName = $pool.image + $yamlImageDir = "D:\" + $yamlImageName + $found = $true + break + } + } + if ($found) { break } + } + } + + if (-not $found) { + Write-Log -message "Node name not found in YAML!!" -severity 'ERROR' + # $SETPXE = $true + } + + Write-Log -message "=== Configuration Comparison ===" -severity 'INFO' + + if ($localPool -eq $WorkerPool) { + Write-Log -message "Worker Pool Match: $WorkerPool" -severity 'INFO' + } + else { + Write-Log -message "Worker Pool MISMATCH!" -severity 'ERROR' + #$SETPXE = $true + } + + if ([string]::IsNullOrWhiteSpace($yamlHash)) { + Write-Log -message "YAML hash is missing or invalid. Treating as mismatch." -severity 'ERROR' + $SETPXE = $true + } + elseif ($localHash -ne $yamlHash) { + Write-Log -message "Git Hash MISMATCH!" -severity 'ERROR' + Write-Log -message "Local: $localHash" -severity 'WARN' + Write-Log -message "YAML : $yamlHash" -severity 'WARN' + $SETPXE = $true + } + else { + Write-Log -message "Git Hash Match: $yamlHash" -severity 'INFO' + } + + if (!(Test-Path $yamlImageDir)) { + Write-Log -message "Image Directory MISMATCH! YAML: $yamlImageDir NOT FOUND" -severity 'ERROR' + $SETPXE = $true + } + + if ($SETPXE) { + + Write-Log -message "Configuration mismatch detected. Evaluating worker-status.json..." -severity 'WARN' + + $searchPaths = @( + "C:\WINDOWS\SystemTemp", + $env:TMP, + $env:TEMP, + $env:USERPROFILE + ) + + $workerStatus = $null + foreach ($path in $searchPaths) { + if ($null -ne $path) { + $candidate = Join-Path $path "worker-status.json" + if (Test-Path $candidate) { + $workerStatus = $candidate + break + } + } + } + + if (-not $workerStatus) { + Write-Log -message "worker-status.json not found. Rebooting now!" -severity 'ERROR' + Set-PXEPendingFlag -Clear + Restart-Computer -Force + return + } + + try { + $json = Get-Content $workerStatus -Raw | ConvertFrom-Json + } + catch { + Write-Log -message "worker-status.json is unreadable. Rebooting now!" -severity 'ERROR' + Set-PXEPendingFlag -Clear + Restart-Computer -Force + return + } + + if (($json.currentTaskIds).Count -eq 0) { + Write-Log -message "No active tasks. Rebooting now!" -severity 'WARN' + Set-PXEPendingFlag -Clear + Restart-Computer -Force + return + } + else { + $task = $json.currentTaskIds[0] + Write-Log -message "Task $task is active. PXE/Reboot deferred until task completion." -severity 'INFO' + + # Record pending reboot/PXE + Set-PXEPendingFlag + + # Prepare PXE boot + Set-PXE + return + } + } + + Write-Log -message "SETPXE set to: $SETPXE" -severity 'DEBUG' + } + + end { + Write-Log -message ('{0} :: end - {1:o}' -f $($MyInvocation.MyCommand.Name), (Get-Date).ToUniversalTime()) -severity 'DEBUG' + } +} + +$bootstrap_stage = (Get-ItemProperty -path "HKLM:\SOFTWARE\Mozilla\ronin_puppet").bootstrap_stage +If ($bootstrap_stage -eq 'complete') { + CompareConfig +} else { + Write-Log -message ('{0} :: Bootstrap has not completed. EXITING!' -f $($MyInvocation.MyCommand.Name)) -severity 'DEBUG' + Exit-PSSession +} diff --git a/modules/win_scheduled_tasks/manifests/gw_exe_check.pp b/modules/win_scheduled_tasks/manifests/gw_exe_check.pp index 833ba77a4..5aa5c994d 100644 --- a/modules/win_scheduled_tasks/manifests/gw_exe_check.pp +++ b/modules/win_scheduled_tasks/manifests/gw_exe_check.pp @@ -5,22 +5,23 @@ class win_scheduled_tasks::gw_exe_check ( ) { - $gw_exe_check_ps1 = "${facts['custom_win_roninprogramdata']}\\gw_exe_check.ps1" + $gw_exe_check_ps1 = "${facts['custom_win_roninprogramdata']}\\gw_exe_check.ps1" - file { $gw_exe_check_ps1: - content => file('win_scheduled_tasks/gw_exe_check.ps1'), - } - # Resource from puppetlabs-scheduled_task - scheduled_task { 'gw_exe_check': - ensure => 'present', - command => "${facts['custom_win_system32']}\\WindowsPowerShell\\v1.0\\powershell.exe", - arguments => "-executionpolicy bypass -File ${gw_exe_check_ps1}", - enabled => true, - trigger => [{ - 'schedule' => 'boot', - 'minutes_interval' => '0', - 'minutes_duration' => '0' - }], - user => 'system', - } + file { $gw_exe_check_ps1: + content => file('win_scheduled_tasks/gw_exe_check.ps1'), + } + + scheduled_task { 'gw_exe_check': + ensure => 'present', + command => "${facts['custom_win_system32']}\\WindowsPowerShell\\v1.0\\powershell.exe", + arguments => "-executionpolicy bypass -File ${gw_exe_check_ps1}", + enabled => true, + trigger => [{ + schedule => 'daily', + start_time => '00:00', + minutes_interval => 60, + #minutes_duration => 1440, # 24 hours = repeat every 5 minutes all day + }], + user => 'SYSTEM', + } } diff --git a/modules/win_scheduled_tasks/manifests/onedrive_task_deletion.pp b/modules/win_scheduled_tasks/manifests/onedrive_task_deletion.pp new file mode 100644 index 000000000..c00e2998a --- /dev/null +++ b/modules/win_scheduled_tasks/manifests/onedrive_task_deletion.pp @@ -0,0 +1,26 @@ +class win_scheduled_tasks::onedrive_task_deletion { + $onedrive_task_deletion_ps = "${facts['custom_win_roninprogramdata']}\\onedrive_task_deletion.ps1" + + if $facts['os']['name'] == 'Windows' { + + file { $onedrive_task_deletion_ps: + ensure => file, + content => file('win_scheduled_tasks/onedrive_task_deletion.ps1'), + } + + scheduled_task { 'one_drive_task_deletion': + ensure => present, + command => "${facts['custom_win_system32']}\\WindowsPowerShell\\v1.0\\powershell.exe", + arguments => "-executionpolicy bypass -File \"${onedrive_task_deletion_ps}\"", + enabled => true, + trigger => [{ + schedule => boot, + }], + user => 'SYSTEM', + require => File[$onedrive_task_deletion_ps], + } + + } else { + fail("${module_name} does not support ${facts['os']['name']}") + } +} diff --git a/modules/win_scheduled_tasks/manifests/self_redeploy_check.pp b/modules/win_scheduled_tasks/manifests/self_redeploy_check.pp new file mode 100644 index 000000000..271fb5885 --- /dev/null +++ b/modules/win_scheduled_tasks/manifests/self_redeploy_check.pp @@ -0,0 +1,28 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +class win_scheduled_tasks::self_redeploy_check ( +) { + + $script = "${facts['custom_win_roninprogramdata']}\\self_redeploy_check.ps1" + + file { $script: + content => file('win_scheduled_tasks/self_redeploy_check.ps1'), + } + + scheduled_task { 'self_redeploy_check': + ensure => 'present', + command => "${facts['custom_win_system32']}\\WindowsPowerShell\\v1.0\\powershell.exe", + arguments => "-executionpolicy bypass -File ${script}", + enabled => true, + trigger => [{ + schedule => 'daily', + start_time => '00:00', + minutes_interval => 120, + #minutes_interval => 5, + #minutes_duration => 1440, # 24 hours = repeat every 2 hours all day + }], + user => 'SYSTEM', + } +}