diff --git a/StackMonkey_Beta.ps1 b/StackMonkey_Beta.ps1
new file mode 100644
index 0000000..44a340e
--- /dev/null
+++ b/StackMonkey_Beta.ps1
@@ -0,0 +1,1648 @@
+#region changes to be done
+
+# seems like the command IS running without UI
+# & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com').Content )) -N8nPassword 'Tndmeeisdwge!' -FetchSitesOnly
+
+# and iwr sm.svstools.com | iex lauched the UI as intended
+
+# need to test
+
+# Write-Host "π οΈ SAMY - Script Automation Monkey (Yeah!)" -ForegroundColor Cyan
+
+#endregion changes to be done
+
+<#
+.SYNOPSIS
+ ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface,
+ and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment.
+
+.DESCRIPTION
+ Provides an HTTP-hosted GUI for selecting and running tasks (module installs, tweaks, offboarding, etc.).
+ When invoked with the correct parameters, it can silently install the SVSMSP toolkit and perform a headless
+ DattoRMM deployment without ever launching the browser or UI.
+
+.PARAMETER SilentInstall
+ Runs only the SVSMSP module install (Install-Toolkit) and skips launching the browser/UI.
+
+.PARAMETER DattoApiUrl
+ The Datto Automate API base URL for headless deployment.
+
+.PARAMETER DattoApiKey
+ Your Datto Automate API username.
+
+.PARAMETER DattoApiSecretKey
+ Your Datto Automate API password/secret.
+
+.PARAMETER SiteUID
+ The target Datto site UID for headless installation.
+
+.PARAMETER SiteName
+ The target Datto site name for headless installation.
+
+.PARAMETER PushSiteVars
+ Switch to include site variables in the headless DattoRMM install.
+
+.PARAMETER InstallRMM
+ Switch to install the RMM agent in the headless DattoRMM install.
+
+.PARAMETER SaveCopy
+ Switch to download the RMM installer executable during the headless DattoRMM install.
+
+.EXAMPLE
+ & ([ScriptBlock]::Create(
+ (iwr 'https://sm.svstools.com' -UseBasicParsing).Content
+ )) `
+ -N8nPassword 'pwd' `
+ -SiteUID 'site-123' `
+ -SiteName 'Acme Corp' `
+ -InstallRMM `
+ -PushSiteVars `
+ -SaveCopy `
+ -WhatIf
+
+.EXAMPLE
+ & ([ScriptBlock]::Create(
+ (iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content
+)) `
+ -N8nPassword 's3cr3t' `
+ -FetchSitesOnly `
+ -OutputFile 'sites.json'
+
+.EXAMPLE
+ & ([ScriptBlock]::Create(
+ (iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content
+)) `
+ -N8nPassword 's3cr3t' `
+ -FetchSitesOnly
+# β writes datto_sites.csv
+
+.EXAMPLE
+ & ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall
+
+.EXAMPLE
+ & ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup
+
+
+.EXAMPLE
+ not tested but i thin this is how would call it
+ iex (iwr 'https://your.server/ScriptMonkey.ps1' -UseBasicParsing).Content; Invoke-ScriptMonkey -DattoApiUrl
+ 'https://β¦' -DattoApiKey 'β¦' -DattoApiSecretKey 'β¦' -SiteUID 'β¦' -SiteName 'β¦' -InstallRMM -PushSiteVars"
+
+#>
+
+ function Invoke-ScriptMonkey {
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # PARAMETERS + GLOBAL VARIABLES
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ [CmdletBinding(
+ DefaultParameterSetName='UI',
+ SupportsShouldProcess=$true,
+ ConfirmImpact= 'Medium'
+ )]
+ param(
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # Toolkit-only mode
+ [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall,
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # remove Toolkit
+ [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup,
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # Datto headless mode
+
+ # Both Datto sets share the webhook password
+ # Shared webhook password for both Datto modes
+ [Parameter(Mandatory,ParameterSetName='DattoFetch')]
+ [Parameter(Mandatory,ParameterSetName='DattoInstall')]
+ [string]$N8nPassword,
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # Fetch only set write sites and exit
+ [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesOnly,
+ [Parameter(ParameterSetName='DattoFetch')][string] $OutputFile = 'datto_sites.csv',
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # Install set: target site must be provided
+ [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID,
+ [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName,
+ [Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars,
+ [Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM,
+ [Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy
+ )
+
+ #region global variables
+
+ # Listening port for HTTP UI
+ $Port = 8082
+
+ # Configurable endpoints
+ $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
+
+ # 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()
+ }
+
+ #endregion global variables
+
+ #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 SVS Module
+
+ #region Get-DattoApiCredentials
+ function Get-DattoApiCredentials {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory)][string]$Password
+ )
+ $headers = @{ "SVSMSPKit" = $Password }
+ try {
+ $resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl `
+ -Headers $headers `
+ -Method GET
+ return @{
+ ApiUrl = $resp.ApiUrl
+ ApiKey = $resp.ApiKey
+ ApiSecretKey = $resp.ApiSecretKey
+ }
+ }
+ catch {
+ Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" Error DattoAuth
+ return $null
+ }
+ }
+
+ #endregion Get-DattoApiCredentials
+
+ #region Get-DattoRMMSites
+ function Get-DattoRmmSites {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory)]
+ [string] $Password,
+
+ [Parameter()]
+ [string] $WebhookUrl = $Global:DattoWebhookUrl
+ )
+
+ # 1) Fetch Datto API credentials from your webhook
+ Write-Verbose "Fetching Datto API credentials from $WebhookUrl"
+ try {
+ $headers = @{ 'SVSMSPKit' = $Password }
+ $creds = Invoke-RestMethod -Uri $WebhookUrl -Headers $headers -Method GET
+ }
+ catch {
+ Throw "Failed to fetch credentials from webhook: $_"
+ }
+
+ $apiUrl = $creds.ApiUrl
+ $apiKey = $creds.ApiKey
+ $apiSecretKey = $creds.ApiSecretKey
+
+ # 2) Request an OAuth token
+ Write-Verbose "Requesting OAuth token from $apiUrl/auth/oauth/token"
+ try {
+ $securePwd = ConvertTo-SecureString -String 'public' -AsPlainText -Force
+ $credObj = New-Object System.Management.Automation.PSCredential('public-client', $securePwd)
+
+ $tokenResp = Invoke-RestMethod `
+ -Uri "$apiUrl/auth/oauth/token" `
+ -Credential $credObj `
+ -Method 'POST' `
+ -ContentType 'application/x-www-form-urlencoded' `
+ -Body "grant_type=password&username=$apiKey&password=$apiSecretKey"
+
+ $token = $tokenResp.access_token
+ }
+ catch {
+ Throw "Failed to obtain OAuth token: $_"
+ }
+
+ # 3) Fetch the list of RMM sites
+ Write-Verbose "Fetching RMM sites from $apiUrl/api/v2/account/sites"
+ try {
+ $authHeader = @{ Authorization = "Bearer $token" }
+ $sitesResp = Invoke-RestMethod `
+ -Uri "$apiUrl/api/v2/account/sites" `
+ -Method 'GET' `
+ -Headers $authHeader `
+ -ContentType 'application/json'
+
+ $siteList = $sitesResp.sites | Select-Object `
+ @{ Name = 'Name'; Expression = { $_.name } }, `
+ @{ Name = 'UID'; Expression = { $_.uid } }
+
+ if (-not $siteList) {
+ Write-Warning "No sites were returned by the API."
+ return @()
+ }
+
+ return $siteList
+ }
+ catch {
+ Throw "Failed to fetch sites from API: $_"
+ }
+ }
+ #endregion Get-DattoRMMSites
+
+ #region Write-Log
+
+ # This function is used as a fallback if the SVSMSP module is not installed
+ 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, [string]$LogFile, [switch]$PassThru
+ )
+ # IDs & colors
+ $idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }
+ $colMap= @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" }
+ $EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] }
+ $color = $colMap[$Level]
+ $fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)"
+ Write-Host $fmt -ForegroundColor $color
+
+ # cache
+ if (-not $Global:LogCache) { $Global:LogCache = @() }
+ $entry = [pscustomobject]@{ Timestamp=(Get-Date -Format "yyyy-MM-dd HH:mm:ss"); Level=$Level; Message=$fmt }
+ $Global:LogCache += $entry
+
+ # file
+ if ($PSBoundParameters.LogFile) {
+ try { "$($entry.Timestamp) $fmt" | Out-File $LogFile -Append -Encoding UTF8 }
+ catch { Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow }
+ }
+
+ # event log
+ if ($LogToEvent) {
+ $etype = if ($Level -in 'Warning','Error') { $Level } else { 'Information' }
+ try {
+ if (-not (Get-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue)) {
+ New-EventLog -LogName $EventLog -Source $EventSource
+ }
+ $msg = "TaskCategory:$TaskCategory | Message:$Message"
+ Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $etype -EventID $EventID -Message $msg
+ } catch { Write-Host "[Warning] EventLog failed: $_" -ForegroundColor Yellow }
+ }
+
+ if ($PassThru) { return $entry }
+ }
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # WRITE-LOG HYBRID (single definition, chooses at runtime if we use the
+ # Write-Log from the module or the built-in Write-LogHelper funtions )
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ function Write-LogHybrid {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory=$true)][string]$Message,
+ [ValidateSet("Info","Warning","Error","Success","General")]
+ [string]$Level = "Info",
+ [string]$TaskCategory = "GeneralTask",
+ [switch]$LogToEvent
+ )
+
+ if ( Get-Command -Name Write-Log -ErrorAction SilentlyContinue ) {
+ # SVSMSP module's Write-Log is available
+ Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent
+ }
+ else {
+ # fall back to your helper
+ Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent
+ }
+ }
+
+
+ #endregion Write-Log
+
+ #region building the Menus
+
+ # 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
+ @{ 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 building the Menus
+
+ #region Build-Checkboxes
+ function Build-Checkboxes {
+ param($Page, $Column)
+
+ (
+ $Global:Tasks |
+ Where-Object Page -EQ $Page |
+ Where-Object Column -EQ $Column |
+ ForEach-Object {
+ $taskId = $_.Id
+ $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
+ " title='$($_.Tooltip)'"
+ } else { '' }
+
+ $html = ""
+
+ if ($_.SubOptions) {
+ # join inside the code block is fine
+ $subHtml = (
+ $_.SubOptions |
+ ForEach-Object {
+ ""
+ }
+ ) -join "`n"
+
+ $html += @"
+
+$subHtml
+
+"@
+ }
+
+ $html
+ }
+ ) -join "`n"
+ }
+
+ #endregion Build-Checkboxes
+
+ ### 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 "Module Version: $($mod.Version)
"
+ }
+ return "SVSMSP_Module 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"
+ }
+ }
+
+ #region UIHtml
+function Get-UIHtml {
+ param([string]$Page = 'onboard')
+
+# no spaces before $style
+$style = @'
+
+'@
+
+# no spaces before $script
+$script = @'
+
+
+'@
+
+# no spaces before $htmlTemplate
+$htmlTemplate = @"
+
+
+
+
+
+Script Monkey
+
+$style
+
+
+
+
+
+

+ {{moduleVersion}}
+
+
+
+
+

+
Script Automation Monkey (Yeah!)
+
+
+
+
+
+
+
+
+
+
On-Boarding
+
This new deployment method ensures everything is successfully deployed with greater ease!
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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('{{moduleVersion}}', (Get-ModuleVersionHtml))
+ $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 UIHtml
+
+ #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()
+ }
+
+ function Handle-FetchSites {
+ param($Context)
+
+ # 1) Read incoming JSON (using block auto-disposes the reader)
+
+ $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 {
+ $creds = Get-DattoApiCredentials -Password $pw
+ if (-not $creds) {
+ Write-LogHybrid "Webhook returned no credentials" Error FetchSites
+ returnRespondEmpty $Context 403
+ return
+ }
+
+ # reuse the same globals from the entrypoint
+ $Global:ApiUrl = $creds.ApiUrl
+ $Global:ApiKey = $creds.ApiKey
+ $Global:ApiSecretKey = $creds.ApiSecretKey
+
+ Write-LogHybrid "Fetched and stored API credentials." Success FetchSites
+ } catch {
+ Write-LogHybrid "Credential-fetch error: $($_.Exception.Message)" Error FetchSites -LogToEvent
+ returnRespondEmpty $Context 500
+ 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 Handler Stubs
+
+ # 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'
+ }
+
+ #region EntryPoint: Define Invoke-ScriptMonkey
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # 3) MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI)
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ switch ($PSCmdlet.ParameterSetName) {
+ 'Toolkit' {
+ Write-LogHybrid "Toolkit-only mode" Info Startup
+ Install-SVSMSP -InstallToolkit
+ return
+ }
+
+ 'Cleanup' {
+ Write-LogHybrid "Running Toolkit cleanup mode" Info Startup
+ Install-SVSMSP -Cleanup
+ return
+ }
+
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+ # 2) If user only wants the site list, do that and exit
+ # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
+
+ 'DattoFetch' {
+ Write-LogHybrid "Fetching site list onlyβ¦" Info DattoAuth
+ $sites = Get-DattoRmmSites -Password $N8nPassword
+
+ if ($SaveSitesOnly) {
+ # If SaveSitesOnly is true, save the output to a file on desktop
+ $desktopPath = [System.Environment]::GetFolderPath('Desktop')
+ $filePath = Join-Path -Path $desktopPath -ChildPath $OutputFile
+
+ $ext = [IO.Path]::GetExtension($OutputFile).ToLower()
+ if ($ext -eq '.json') {
+ # Save the file to the desktop using the full path
+ $sites | ConvertTo-Json -Depth 3 | Out-File -FilePath $filePath -Encoding UTF8
+ } else {
+ # Save the file to the desktop using the full path
+ $sites | Export-Csv -Path $filePath -NoTypeInformation -Encoding UTF8
+ }
+
+ Write-LogHybrid "Wrote $($sites.Count) sites to $filePath" Success DattoAuth
+
+ } else {
+ # If SaveSitesOnly is not true, just fetch sites (for UI purposes or silent mode without saving)
+ Write-LogHybrid "Sites fetched successfully, but not saved." Success DattoAuth
+ }
+ return
+ }
+
+
+ # ββββββββββββββββββββββββββββββββββββββββββββ
+ # 3) Invoke the existing Install-DattoRMM cmdlet
+ # ββββββββββββββββββββββββββββββββββββββββββββ
+
+ 'DattoInstall' {
+ Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth
+ if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) {
+ Install-DattoRMM `
+ -ApiUrl $Global:ApiUrl `
+ -ApiKey $Global:ApiKey `
+ -ApiSecretKey $Global:ApiSecretKey `
+ -SiteUID $SiteUID `
+ -SiteName $SiteName `
+ -PushSiteVars:$PushSiteVars `
+ -InstallRMM:$InstallRMM `
+ -SaveCopy:$SaveCopy
+ }
+
+ return
+
+ }
+
+ 'UI' {
+ Write-LogHybrid "Launching UI" Info Startup
+ Write-Host "Starting ScriptMonkey UI on http://localhost:$Port/" -ForegroundColor Cyan
+ Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port"
+ Start-Server # blocks until you click Exit
+ return
+ }
+
+ }
+ #endregion EntryPoint: Define Invoke-ScriptMonkey
+
+ Write-Host "π οΈ SAMY - Script Automation Monkey (Yeah!)" -ForegroundColor Cyan
+ Write-Host "ParameterSetName: $($PSCmdlet.ParameterSetName)" -ForegroundColor Yellow
+
+ #region β guarantee NuGet provider is present without prompting
+
+ # βββ Top of script βββ
+ Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
+ Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
+
+ # βββ ensure TLS 1.2 + no prompts βββ
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ $ProgressPreference = 'SilentlyContinue'
+ $ConfirmPreference = 'None'
+
+ # check if NuGet exists (no outputβassigned to $nuget)
+ $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
+
+ if (-not $nuget) {
+ # install it (again, assignment suppresses the table)
+ Install-PackageProvider `
+ -Name NuGet `
+ -MinimumVersion 2.8.5.201 `
+ -Force `
+ -Confirm:$false
+
+
+ # re-query just for version info
+ $found = Get-PackageProvider -Name NuGet -ListAvailable
+ Write-Host "Installed NuGet provider v$($found.Version)" -ForegroundColor Green
+ }
+ else {
+ Write-Host "NuGet provider already present (v$($found.Version))" -ForegroundColor DarkGray
+ }
+
+ # now import it silently
+ Import-PackageProvider -Name NuGet -Force -ErrorAction SilentlyContinue | Out-Null
+
+ # ensure trust PSGallery without its own output (so you don't get βuntrusted repositoryβ prompt
+ $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
+ if ($gallery.InstallationPolicy -ne 'Trusted') {
+ Set-PSRepository `
+ -Name PSGallery `
+ -InstallationPolicy Trusted `
+ -ErrorAction SilentlyContinue | Out-Null
+
+ Write-Host "PSGallery marked as Trusted" -ForegroundColor Green
+ }
+
+ #endregion
+
+ # If we got here, it's the UI setβlaunch browser + listener:
+ # βββ UI fallback starts here βββ
+ Write-LogHybrid "Launching UI" Info Startup
+
+ #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 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
+ }
+
+
+ #endregion
+
+
+ }
+
+ if ($MyInvocation.InvocationName -eq '.') {
+ # dot-sourced, don't invoke
+} elseif ($PSCommandPath) {
+ # script was saved and run directly
+ Invoke-ScriptMonkey @PSBoundParameters
+} else {
+ # iwr | iex fallback
+ if ($args.Count -gt 0) {
+ # Convert -Param value -Switch into a hashtable for splatting
+ $namedArgs = @{}
+ for ($i = 0; $i -lt $args.Count; $i++) {
+ if ($args[$i] -is [string] -and $args[$i].StartsWith('-')) {
+ $key = $args[$i].TrimStart('-')
+ $next = $args[$i + 1]
+ if ($next -and ($next -notlike '-*')) {
+ $namedArgs[$key] = $next
+ $i++ # Skip next one, it's the value
+ } else {
+ $namedArgs[$key] = $true
+ }
+ }
+ }
+ Invoke-ScriptMonkey @namedArgs
+ } else {
+ Invoke-ScriptMonkey
+ }
+}
+
+