# 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';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=[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() } # 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($Page, $Column) ( $Global:Tasks | Where-Object Page -EQ $Page | Where-Object Column -EQ $Column | ForEach-Object { $taskId = $_.Id $html = "" if ($_.SubOptions) { $subHtml = ( $_.SubOptions | ForEach-Object { "" } ) -join "`n" $html += @"
"@ } $html } ) -join "`n" } ### Get SVSMSP module version to display in the UI function Get-ModuleVersionHtml { $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1 if ($mod) { return "