From 80fbd74fd9220cd5116565ca5d1d0e8609ebd071 Mon Sep 17 00:00:00 2001 From: Stephan Yelle Date: Wed, 4 Jun 2025 21:21:49 -0400 Subject: [PATCH] Tweaks tab --- StackMonkey.ps1 | 1453 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1453 insertions(+) create mode 100644 StackMonkey.ps1 diff --git a/StackMonkey.ps1 b/StackMonkey.ps1 new file mode 100644 index 0000000..0f4cc6e --- /dev/null +++ b/StackMonkey.ps1 @@ -0,0 +1,1453 @@ +# region changes to be done + + +#endregion + +# STACK = Scripted Tooling for Automated Client Kickoff +# MONKEY = Module-based Onboarding & Next-step Kickoff Engine Yoke +# Conveys the idea of coupling tasks together and keeping them under control. +#region Config & Task Definitions + +# Listening port for HTTP UI +$Port = 8082 + +# Configurable endpoints +$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' + + +# Define every task once here: +# Id → checkbox HTML `id` +# Name → URL path (`/Name`) +# Label → user-visible text +# HandlerFn → the PowerShell function to invoke +# Page → which tab/page it appears on + +$Global:Tasks = @( + # On-Boarding, left column + @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' }, + @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' }, + @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' }, + @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' }, + @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' }, + @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' }, + @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left'; + SubOptions= @( + @{ Value='inputVar'; Label='Copy Site Variables' }, + @{ Value='rmm'; Label='Install RMM Agent' }, + @{ Value='exe'; Label='Download Executable' } + ) + }, + + + # On-Boarding, right column (optional bits) + @{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' }, + # @{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' }, + + # Off-Boarding + @{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' }, + @{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' }, + + # ───────────────────────────────────────────────────────────────────────────── + # Tweaks: one entry per PS1 file, with hidden
of radio‐buttons for params + # ───────────────────────────────────────────────────────────────────────────── + + @{ + Id = 'setEdgeDefaultSearch'; + Name = 'setEdgeDefaultSearch'; + Label = 'Set Edge Default Search'; + Tooltip = 'Choose which search engine Edge should use (or Reset to defaults)'; + HandlerFn = 'Handle-SetEdgeDefaultSearch'; + Page = 'tweaks'; + Column = ''; + SubOptions = @( + @{ + ParamName = 'SearchProviderName'; + Type = 'radio'; + Options = @('Google', 'Bing', 'DuckDuckGo'); + Default = 'Google' + }, + @{ + ParamName = 'Reset'; + Type = 'radio'; + Options = @('No', 'Yes'); + Default = 'No' + } + ) + }, + + @{ + Id = 'setTextEditor'; + Name = 'setTextEditor'; + Label = 'Set Default Text Editor'; + Tooltip = 'Pick your preferred default text editor for .txt and .ps1 files.'; + HandlerFn = 'Handle-SetTextEditor'; + Page = 'tweaks'; + Column = ''; + SubOptions = @( + @{ + ParamName = 'Editor'; + Type = 'radio'; + Options = @('Notepad', 'Notepad++', 'VSCode'); + Default = 'Notepad' + } + ) + }, + + @{ + Id = 'setWindowsPerformance'; + Name = 'setWindowsPerformance'; + Label = 'Optimize Windows Performance'; + Tooltip = 'Choose performance mode or restore defaults.'; + HandlerFn = 'Handle-SetWindowsPerformance'; + Page = 'tweaks'; + Column = ''; + SubOptions = @( + @{ + ParamName = 'Mode'; + Type = 'radio'; + Options = @('Gaming', 'Balanced', 'PowerSaver'); + Default = 'Balanced' + }, + @{ + ParamName = 'RestoreDefaults'; + Type = 'radio'; + Options = @('No', 'Yes'); + Default = 'No' + } + ) + }, + + @{ + Id = 'stopUnnecessaryServices'; + Name = 'stopUnnecessaryServices'; + Label = 'Stop Unnecessary Services'; + Tooltip = 'Log actions to file or just run silently.'; + HandlerFn = 'Handle-StopUnnecessaryServices'; + Page = 'tweaks'; + Column = ''; + SubOptions = @( + @{ + ParamName = 'LogFilePathChoice'; + Type = 'radio'; + Options = @('NoLogging', 'DefaultPath', 'CustomPath'); + Default = 'NoLogging' + } + # If you want to allow a “custom path” text‐box, you can detect when "CustomPath" is selected + # on the front‐end and show an . (See note below.) + ) + }, + + @{ + Id = 'setBrowserPwdPolicy'; + Name = 'setBrowserPwdPolicy'; + Label = 'Enforce Browser Password Policy'; + Tooltip = 'Enable or disable saving passwords and set expiration days.'; + HandlerFn = 'Handle-SetBrowserPwdPolicy'; + Page = 'tweaks'; + Column = ''; + SubOptions = @( + @{ + ParamName = 'DisableSavePasswords'; + Type = 'radio'; + Options = @('No', 'Yes'); + Default = 'No' + }, + @{ + ParamName = 'PasswordExpirationDays'; + Type = 'radio'; + Options = @('30', '60', '90', 'Never'); + Default = '90' + } + ) + }, + + + # SVS Apps + @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' } + +) + +#endregion + +#region Logging Helpers + +# Initialize a global in-memory log cache +if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() +} + +# Core Write-Log function (advanced with event-log support) +function Write-LogHelper { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "SVSMSP_Module", + [string]$EventLog = "Application", + [int]$CustomEventID + ) + $EventID = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }[$Level] + $Icon = @{Info=[System.Char]::ConvertFromUtf32(0x1F4CB);Warning=[char]0x26A0;Error=[char]0x274C;Success=[char]0x2705;General=[char]0x1F4E6}[$Level] + $logEntry = [PSCustomObject]@{ + Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + Level = $Level + Message = "$Icon [$Level] [$TaskCategory] $Message (EventID:$EventID)" + } + [void]$Global:LogCache.Add($logEntry) + + if ($LogToEvent) { + try { + if (-not (Get-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue)) { + New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue + } + Write-EventLog -LogName $EventLog -Source $EventSource ` + -EntryType $Level -EventId $EventID ` + -Message $Message + } catch { + Write-Host "$([System.Char]::ConvertFromUtf32(0x26A0))$([System.Char]::ConvertFromUtf32(0xFE0F)) [Warning] [EventLog] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow + + + } + } +} + +# Hybrid wrapper: uses your module's Write-Log if available, else falls back +if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + function Write-LogHybrid { param($Message,$Level,$TaskCategory,$LogToEvent) Write-Log @PSBoundParameters } +} else { + function Write-LogHybrid { param($Message,$Level,$TaskCategory,$LogToEvent) Write-LogHelper @PSBoundParameters } +} + +#endregion + +#region Handler Stubs + +function Respond-Text { + param($Context, $Text) + $bytes = [Text.Encoding]::UTF8.GetBytes($Text) + $Context.Response.ContentType = 'text/plain' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) + $Context.Response.OutputStream.Close() +} + +function Respond-HTML { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)][object] $Context, + [Parameter(Mandatory = $true)][string] $Html + ) + $bytes = [Text.Encoding]::UTF8.GetBytes($Html) + $Context.Response.ContentType = 'text/html' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Context.Response.OutputStream.Close() +} + +# new helper to return JSON +function Respond-JSON { + param($Context, $Object) + $json = $Object | ConvertTo-Json -Depth 5 + $bytes = [Text.Encoding]::UTF8.GetBytes($json) + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) + $Context.Response.OutputStream.Close() +} +#region Get-DattoApiCreds +function Get-DattoApiCredentials { + param ([string]$Password) + $url = "https://automate.svstools.ca/webhook/svsmspkit" + $headers = @{ "SVSMSPKit" = $Password } + try { + $response = Invoke-RestMethod -Uri $url -Headers $headers -Method GET + return @{ + ApiUrl = $response.ApiUrl + ApiKey = $response.ApiKey + ApiSecretKey = $response.ApiSecretKey + } + } catch { + Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" "Error" "DattoAuth" + return $null + } +} +#endregion + +#region Install-DattoRMM-Helper +function Install-DattoRMM-Helper { + param ( + [string]$ApiUrl, + [string]$ApiKey, + [string]$ApiSecretKey, + [switch]$FetchSitesOnly, + [string]$SiteName, + [string]$SiteUID + ) + if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) { + Write-LogHybrid -Message "Missing required parameters. Please provide ApiUrl, ApiKey, and ApiSecretKey." -Level "Error" -LogToEvent + return + } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Write-LogHybrid -Message "Fetching OAuth token..." -Level "Info" + try { + $securePassword = ConvertTo-SecureString -String 'public' -AsPlainText -Force + $apiGenToken = Invoke-WebRequest -Credential (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('public-client', $securePassword)) ` + -Uri ('{0}/auth/oauth/token' -f $ApiUrl) ` + -Method 'POST' ` + -ContentType 'application/x-www-form-urlencoded' ` + -Body ('grant_type=password&username={0}&password={1}' -f $ApiKey, $ApiSecretKey) ` + | ConvertFrom-Json + $requestToken = $apiGenToken.access_token + Write-LogHybrid -Message "OAuth token fetched successfully." -Level "Success" -LogToEvent + } catch { + Write-LogHybrid -Message "Failed to fetch OAuth token. Details: $($_.Exception.Message)" -Level "Error" -LogToEvent + return + } + $getHeaders = @{"Authorization" = "Bearer $requestToken"} + if ($FetchSitesOnly) { + Write-Host "Fetching list of sites from the Datto RMM API..." -ForegroundColor Cyan + try { + $getHeaders = @{"Authorization" = "Bearer $requestToken" } + $getSites = Invoke-WebRequest -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $getHeaders -ContentType "application/json" + $sitesJson = $getSites.Content | ConvertFrom-Json + $siteList = $sitesJson.sites | ForEach-Object { + [PSCustomObject]@{ + Name = $_.name + UID = $_.uid + } + } + Write-Host "Successfully fetched list of sites." -ForegroundColor Green + return $siteList + } + catch { + Write-Host "Failed to fetch sites from the API. Details: $($_.Exception.Message)" -ForegroundColor Red + return + } + } +} +#endregion + +#region SVS Module + +function Install-SVSMSP { + param ( + [switch] $Cleanup, + [switch] $InstallToolkit, + [Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }), + [Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }), + [Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP", + [Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo", + [Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/" + ) + + function Perform-Cleanup { + Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule" + # …your old cleanup logic here… + } + + function Perform-ToolkitInstallation { + Perform-Cleanup + Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule" + if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) { + Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted + } + Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule" + Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force + Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" + } + + Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" + if ($Cleanup) { + Perform-Cleanup; return + } + if ($InstallToolkit) { + Perform-ToolkitInstallation; return + } + # default if no switch passed: + Perform-ToolkitInstallation +} + +#endregion + +# POST /getpw → read JSON body, call helper, return JSON + + +function Handle-FetchSites { + param($Context) + + # 1) Read incoming JSON (using block auto-disposes the reader) + + + <# powershell v7 + using ($reader = [IO.StreamReader]::new($Context.Request.InputStream)) { + $raw = $reader.ReadToEnd() + } + try { + $pw = (ConvertFrom-Json $raw).password + if (-not $pw) { throw "Missing `password` field" } + } catch { + Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context 400 + return + } + #> + + $reader = [IO.StreamReader]::new($Context.Request.InputStream) + try { + $raw = $reader.ReadToEnd() + } finally { + $reader.Close() + } + + try { + $pw = (ConvertFrom-Json $raw).password + } catch { + Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context + return + } + + # 2) Fetch your Datto API creds from the webhook + Write-LogHybrid "Calling webhook for Datto credentials…" "Info" "FetchSites" + try { + $hdr = @{ "SVSMSPKit" = $pw } + $resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl -Headers $hdr -Method GET + + # store for later RMM calls + $Global:ApiUrl = $resp.ApiUrl + $Global:ApiKey = $resp.ApiKey + $Global:ApiSecretKey = $resp.ApiSecretKey + + Write-LogHybrid "Fetched and stored API credentials." "Success" "FetchSites" + } catch { + Write-LogHybrid "Webhook call failed: $($_.Exception.Message)" "Error" "FetchSites" -LogToEvent + returnRespondEmpty $Context 403 + return + } + + # 3) Exchange for a bearer token + Write-LogHybrid "Requesting OAuth token" "Info" "FetchSites" + try { + $securePublic = ConvertTo-SecureString 'public' -AsPlainText -Force + $creds = New-Object System.Management.Automation.PSCredential('public-client',$securePublic) + $tokenResp = Invoke-RestMethod ` + -Uri "$Global:ApiUrl/auth/oauth/token" ` + -Credential $creds ` + -Method Post ` + -ContentType 'application/x-www-form-urlencoded' ` + -Body "grant_type=password&username=$Global:ApiKey&password=$Global:ApiSecretKey" + $token = $tokenResp.access_token + Write-LogHybrid "OAuth token acquired." "Success" "FetchSites" + } catch { + Write-LogHybrid "OAuth request failed: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context 500 + return + } + + # 4) Pull the site list + Write-LogHybrid "Fetching Datto RMM site list" "Info" "FetchSites" + try { + $hdr = @{ Authorization = "Bearer $token" } + $sitesResp = Invoke-RestMethod -Uri "$Global:ApiUrl/api/v2/account/sites" ` + -Method Get ` + -Headers $hdr ` + -ContentType 'application/json' + + $siteList = $sitesResp.sites | ForEach-Object { + [PSCustomObject]@{ Name = $_.name; UID = $_.uid } + } + Write-LogHybrid "Site list retrieved ($($siteList.Count) sites)." "Success" "FetchSites" + } catch { + Write-LogHybrid "Failed to fetch site list: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context 500 + return + } + + # 5) Return JSON array + $json = $siteList | ConvertTo-Json -Depth 2 + $bytes = [Text.Encoding]::UTF8.GetBytes($json) + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Context.Response.OutputStream.Close() +} + + + +# Helper function to consistently return an empty JSON array +function returnRespondEmpty { + param( + [Parameter(Mandatory)][object]$Context, + [Parameter(Mandatory)][ValidateRange(100,599)][int]$StatusCode = 500 + ) + # Always return an empty JSON array body + $empty = [Text.Encoding]::UTF8.GetBytes("[]") + + # Set the desired status code and headers + $Context.Response.StatusCode = $StatusCode + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $empty.Length + + # Write and close + $Context.Response.OutputStream.Write($empty, 0, $empty.Length) + $Context.Response.OutputStream.Close() +} + + + +# On-boarding handlers +function Handle-SetSVSPowerPlan { + param($Context) + + # 1) call into your module + Set-SVSPowerPlan + + # 2) log & write back a simple text response + Write-LogHybrid "PowerPlan set" "Success" "OnBoard" + Respond-Text $Context "PowerPlan applied" +} + +function Handle-InstallSVSMSP { + param($Context) + Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" "Info" "OnBoard" + try { + Install-SVSMSP -InstallToolkit + Respond-Text $Context "SVSMSP Module installed/updated." + } catch { + Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard" + Respond-Text $Context "ERROR: $_" + } +} + +function Handle-InstallCyberQP { + param($Context) + + # 1) call into your module + Install-CyberQP + + # 2) log & write back a simple text response + Write-LogHybrid "CyberQP installed" "Success" "OnBoard" + Respond-Text $Context "CyberQP installed" +} + +function Handle-InstallThreatLocker { + param($Context) + + # 1) call into your module + Install-ThreatLocker + + # 2) log & write back a simple text response + Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard" + Respond-Text $Context "ThreatLocker installed" +} + +function Handle-InstallRocketCyber { + param($Context) + + # 1) call into your module + Install-RocketCyber + + # 2) log & write back a simple text response + Write-LogHybrid "RocketCyber installed" "Success" "OnBoard" + Respond-Text $Context "RocketCyber installed" +} + +function Handle-InstallSVSHelpDesk { + param($Context) + + # 1) call into your module + Install-SVSHelpDesk + + # 2) log & write back a simple text response + Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard" + Respond-Text $Context "SVS HelpDesk installed" +} + +function Handle-InstallDattoRMM { + param($Context) + $req = $Context.Request + $resp = $Context.Response + + if ($req.HttpMethod -ne 'POST') { + $resp.StatusCode = 405; $resp.ContentType = 'text/plain' + $resp.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('Use POST'),0,7) + $resp.OutputStream.Close(); return + } + + # parse JSON body + $body = (New-Object IO.StreamReader $req.InputStream).ReadToEnd() + $data = $body | ConvertFrom-Json + $checked = $data.checkedValues + $uid = $data.UID + $name = $data.Name + + try { + Install-DattoRMM ` + -ApiUrl $Global:ApiUrl ` + -ApiKey $Global:ApiKey ` + -ApiSecretKey $Global:ApiSecretKey ` + -SiteUID $uid ` + -SiteName $name ` + -PushSiteVars:($checked -contains 'inputVar') ` + -InstallRMM: ($checked -contains 'rmm') ` + -SaveCopy: ($checked -contains 'exe') + + Write-LogHybrid "RMM install triggered for $name" "Success" "DattoRMM" + $resp.StatusCode = 200 + $responseString = "Triggered DattoRMM for $name" + } + catch { + Write-LogHybrid "Error in Install-DattoRMM: $_" "Error" "DattoRMM" + $resp.StatusCode = 500 + $responseString = "ERROR: $($_.Exception.Message)" + } + + $b = [Text.Encoding]::UTF8.GetBytes($responseString) + $resp.ContentType = 'text/plain' + $resp.ContentLength64 = $b.Length + $resp.OutputStream.Write($b,0,$b.Length) + $resp.OutputStream.Close() +} + + +#-------------------------------------------------------------------------- + # Handle function for the Tweaks tab + #-------------------------------------------------------------------------- + + +function Handle-SetEdgeDefaultSearch { +param($Context) + +# Read which radio was chosen: +# You’ll receive a GET or POST? In the existing pattern, “tweaks” is GET‐only, +# so you can’t get the radio values over GET. You’ll probably want to change +# those “fetch(t.handler, { method: 'GET' })” calls in JS to send a POST +# with JSON containing all the checked‐plus‐radio values. + +# Here’s a simple stub: +Write-LogHybrid "Handle-SetEdgeDefaultSearch called" "Info" "Tweaks" + +# Response back: +Respond-Text $Context "Applied SetEdgeDefaultSearch with whatever was selected." +} + +function Handle-SetTextEditor { + param($Context) + Write-LogHybrid "Handle-SetTextEditor called" "Info" "Tweaks" + Respond-Text $Context "Text editor preference applied." +} + +function Handle-SetWindowsPerformance { + param($Context) + Write-LogHybrid "Handle-SetWindowsPerformance called" "Info" "Tweaks" + Respond-Text $Context "WindowsPerformance tweak applied." +} + +function Handle-StopUnnecessaryServices { + param($Context) + Write-LogHybrid "Handle-StopUnnecessaryServices called" "Info" "Tweaks" + Respond-Text $Context "Stop-UnnecessaryServices tweak applied." +} + +function Handle-SetBrowserPwdPolicy { + param($Context) + Write-LogHybrid "Handle-SetBrowserPwdPolicy called" "Info" "Tweaks" + Respond-Text $Context "Browser password policy tweak applied." +} + + +# Off-boarding handlers +function Handle-UninstallCyberQP { + param($Context) + + # 1) call into your module + Uninstall-CyberQP + + Write-LogHybrid "CyberQP uninstalled" "Success" "OffBoard" + Respond-Text $Context "CyberQP uninstalled" +} + +function Cleanup-SVSMSP { + param($Context) + Write-LogHybrid "SVSMSP cleaned up" "Success" "OffBoard" + Respond-Text $Context "SVSMSP cleaned up" +} + +# Tweaks handler +function Disable-Animations { + param($Context) + Write-LogHybrid "Animations disabled" "Success" "Tweaks" + Respond-Text $Context "Animations disabled" +} + +# SVSApps handler +function Install-WingetLastPass { + param($Context) + Write-LogHybrid "Winget LastPass installed" "Success" "SVSApps" + Respond-Text $Context "Winget LastPass installed" +} + +#endregion + +#region UI Generation + +function Build-Checkboxes { + param( + [string] $Page, + [string] $Column + ) + + # For each task in the given Page & Column, build a