#region Config & Task Definitions # Listening port for HTTP UI $Port = 8082 # 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='Set-SVSPowerPlan'; Page='onboard'; Column='left' }, @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Install-SVSMSPModule'; Page='onboard'; Column='left' }, @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Install-CyberQP'; Page='onboard'; Column='left' }, @{ Id='installSplashtop'; Name='installSplashtop'; Label='Install Splashtop'; HandlerFn='Install-Splashtop'; Page='onboard'; Column='left' }, @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Install-SVSHelpDesk'; Page='onboard'; Column='left' }, @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Install-ThreatLocker'; Page='onboard'; Column='left' }, @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Install-RocketCyber'; Page='onboard'; Column='left' }, @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallRMM'; Page='onboard'; Column='left'; SubOptions= @( @{ Value='inputVar'; Label='Copy Site Variables' }, @{ Value='rmm'; Label='Install DRMM Agent' }, @{ Value='exe'; Label='Download .exe' } ) }, # 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';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 @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' }, # 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="📝"; Warning="⚠️"; Error="❌"; Success="✅"; General="📦" }[$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 "⚠️ [EventLog] Failed to write: $($_.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 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 # POST /getpw → read JSON body, call helper, return JSON function Handle-FetchSites { param($Context) Write-Host "[Debug] Handle-FetchSites invoked" # ← add this try { $body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() $pw = (ConvertFrom-Json $body).password $sites = Install-DattoRMM-Helper ` -ApiUrl $ApiUrl ` -ApiKey $ApiKey ` -ApiSecretKey $ApiSecretKey ` -FetchSitesOnly if (-not $sites) { $Context.Response.StatusCode = 500; $sites = @() } $json = $sites | ConvertTo-Json -Depth 2 $bytes = [Text.Encoding]::UTF8.GetBytes($json) $Context.Response.ContentType = 'application/json' $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) } catch { $Context.Response.StatusCode = 500 $Context.Response.ContentType = 'application/json' $Context.Response.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('[]'),0,2) } finally { $Context.Response.OutputStream.Close() } } # On-boarding handlers function Set-SVSPowerPlan { param($Context) Write-LogHybrid "PowerPlan set" "Success" "OnBoard" Respond-Text $Context "Powerplan applied" } function Install-SVSMSPModule { param($Context) Write-LogHybrid "SVSMSP Module installed" "Success" "OnBoard" Respond-Text $Context "SVSMSP Module installed" } function Install-CyberQP { param($Context) Write-LogHybrid "CyberQP installed" "Success" "OnBoard" Respond-Text $Context "CyberQP installed" } # Off-boarding handlers function Uninstall-CyberQP { param($Context) 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($Page, $Column) ( $Global:Tasks | Where-Object Page -EQ $Page | Where-Object Column -EQ $Column | ForEach-Object { # 1) render the main checkbox $html = "" # 2) if this task has SubOptions, render them in a hidden container if ($_.SubOptions) { Write-Host "👉 Rendering SubOptions for task $($_.Id)" $subHtml = ( $_.SubOptions | ForEach-Object { "" } ) -join "`n" $html += @" "@ } $html } ) -join "`n" } function Get-UIHtml { param([string]$Page = 'onboard') # # 1) Inline your full original CSS here # $style = @' '@ $script = @' '@ # # 3) The HTML skeleton with placeholders # $htmlTemplate = @" SVS TaskGate $style
SVS Logo

On-Boarding

This new deployment method ensures everything is successfully deployed with greater ease!

SVSMSP Stack

{{onboardLeftColumn}}

Optional

{{onboardRightColumn}}

Off-Boarding

{{offboardCheckboxes}}

Tweaks

{{tweaksCheckboxes}}

SVS APPs

{{appsCheckboxes}}
$script
"@ # # 4) Build the checkbox HTML and tasks JS from $Global:Tasks # # On-boarding now has two columns: $onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left' $onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right' # Off-boarding, Tweaks, SVSApps stay one-column: $offboard = Build-Checkboxes -Page 'offboard' -Column '' $tweaks = Build-Checkboxes -Page 'tweaks' -Column '' $apps = Build-Checkboxes -Page 'SVSApps' -Column '' # Tasks JS array (fixed) $tasksJsAll = ( $Global:Tasks | ForEach-Object { " { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }" } ) -join ",`n" # # 5) Inject into template # $html = $htmlTemplate $html = $html.Replace('{{onboardLeftColumn}}', $onboardLeft) $html = $html.Replace('{{onboardRightColumn}}', $onboardRight) $html = $html.Replace('{{offboardCheckboxes}}', $offboard) $html = $html.Replace('{{tweaksCheckboxes}}', $tweaks) $html = $html.Replace('{{appsCheckboxes}}', $apps) $html = $html.Replace('{{tasksJsAll}}', $tasksJsAll) $html = $html.Replace('{{defaultPage}}', $Page) return $html } #endregion #region HTTP Listener & Routing # Handle shutdown command if ($path -eq 'quit') { Write-LogHybrid "Shutdown requested" "Info" "Server" Respond-Text $Context "Server shutting down." # This will break out of the while loop in Start-Server $Global:Listener.Stop() return } # Sends the HTML for a given page or invokes a task handler function Dispatch-Request { param($Context) # figure out the path $path = $Context.Request.Url.AbsolutePath.TrimStart('/') # ---- Shutdown handler ---- if ($path -eq 'quit') { Write-LogHybrid "Shutdown requested" "Info" "Server" Respond-Text $Context "Server shutting down." # stop the listener loop $Global:Listener.Stop() return } # ---- Fetch Sites endpoint ---- if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') { Handle-FetchSites $Context return } # ---- Serve UI pages ---- if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) { $page = if ($path -eq '') { 'onboard' } else { $path } $html = Get-UIHtml -Page $page Respond-HTML $Context $html return } # ---- Task invocation ---- $task = $Global:Tasks | Where-Object Name -EQ $path if ($task) { & $task.HandlerFn $Context return } # ---- 404 ---- $Context.Response.StatusCode = 404 Respond-Text $Context '404 - Not Found' } # Starts the HTTP listener loop function Start-Server { # make it accessible to Dispatch-Request $Global:Listener = [System.Net.HttpListener]::new() $Global:Listener.Prefixes.Add("http://localhost:$Port/") $Global:Listener.Start() Write-Host "Listening on http://localhost:$Port/ ..." try { while ($Global:Listener.IsListening) { $ctx = $Global:Listener.GetContext() try { Dispatch-Request $ctx } catch { Write-LogHybrid "Dispatch error: $_" "Error" "Server" } } } finally { # once the loop exits, clean up $Global:Listener.Close() Write-LogHybrid "Listener closed." "Info" "Server" } } #endregion # Bootstrap: launch the server Start-Server