#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 '' -FetchSitesOnly -OutputFile ''
.EXAMPLE
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content )) -N8nPassword 's3cr3t' -FetchSitesOnly
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup
#>
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
#region Get-ModuleVersionHtml
### 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
"
}
#endregion Get-ModuleVersionHtml
#region Strat-Server
# 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 Strat-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
#region Dispatch-Request
# 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'
}
#endregion Dispatch-Request
#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
#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
#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
}
}