#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
# βββ DattoFetch & DattoInstall share the webhook creds βββββββββββββ
[Parameter(Mandatory,ParameterSetName='DattoFetch')]
[Parameter(Mandatory,ParameterSetName='DattoInstall')]
[switch]$UseWebhook,
[Parameter(Mandatory,ParameterSetName='DattoFetch')]
[Parameter(Mandatory,ParameterSetName='DattoInstall')]
[string]$WebhookPassword,
[string]$WebhookUrl = $Global:DattoWebhookUrl,
# βββ only DattoFetch uses these ββββββββββββββββββββββββββββββββββββ
[Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites,
[Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList,
[Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$','i')][string] $OutputFile = 'datto_sites.csv',
# βββ only DattoInstall uses these βββββββββββββββββββββββββββββββββ
[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 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()
}
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)
try {
# 1) Read the incoming JSON payload (contains only the webhook password)
$raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$pw = (ConvertFrom-Json $raw).password
# 2) Delegate to your unified function
$sites = Install-DattoRMM `
-UseWebhook `
-WebhookPassword $pw `
-FetchSites `
-SaveSitesList:$SaveSitesList `
-OutputFile $OutputFile
# 3) Return JSON array of sites
Respond-JSON $Context $sites
}
catch {
# Log the exception and return HTTP 500
Write-LogHybrid "Handle-FetchSites error: $($_.Exception.Message)" Error DattoRMM
$Context.Response.StatusCode = 500
Respond-Text $Context "Internal server error fetching sites."
}
}
function Handle-InstallDattoRMM {
param($Context)
try {
if ($Context.Request.HttpMethod -ne 'POST') {
$Context.Response.StatusCode = 405
Respond-Text $Context 'Use POST'
return
}
# 1) Read and parse the JSON body
$body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$data = ConvertFrom-Json $body
# 2) Delegate to your unified function for the install
Install-DattoRMM `
-UseWebhook `
-WebhookPassword $Global:WebhookPassword `
-SiteUID $data.UID `
-SiteName $data.Name `
-PushSiteVars:($data.checkedValues -contains 'inputVar') `
-InstallRMM: ($data.checkedValues -contains 'rmm') `
-SaveCopy: ($data.checkedValues -contains 'exe')
# 3) Acknowledge to the client
Respond-Text $Context "Triggered DattoRMM for $($data.Name)"
}
catch {
# Log the exception and return HTTP 500
Write-LogHybrid "Handle-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM
$Context.Response.StatusCode = 500
Respond-Text $Context "Internal server error during DattoRMM install."
}
}
#endregion Handler Stubs
#region Install-DattoRMM
<#
.SYNOPSIS
Installs/configures the Datto RMM agent, fetches site lists, and optionally saves the site list to disk.
.DESCRIPTION
Centralizes Datto RMM operations in one function:
- Fetch API credentials from a webhook (-UseWebhook)
- Acquire OAuth token
- Fetch site list (-FetchSites)
- Save site list to Desktop as JSON or CSV (-FetchSites + -SaveSitesList)
- Write site variables to registry (-PushSiteVars)
- Download & launch the RMM agent installer (-InstallRMM)
- Save a copy of the installer (-SaveCopy)
.PARAMETER UseWebhook
Fetches ApiUrl, ApiKey, and ApiSecretKey from the webhook when used with WebhookPassword.
.PARAMETER WebhookPassword
Password for authenticating to the credentials webhook.
.PARAMETER WebhookUrl
URL of the credentials webhook. Defaults to $Global:DattoWebhookUrl.
.PARAMETER ApiUrl
Direct Datto API endpoint URL (if not using webhook).
.PARAMETER ApiKey
Direct Datto API key (if not using webhook).
.PARAMETER ApiSecretKey
Direct Datto API secret (if not using webhook).
.PARAMETER FetchSites
Fetches the list of sites and skips all install steps.
.PARAMETER SaveSitesList
Saves the fetched site list to Desktop using OutputFile. Must be used with -FetchSites.
.PARAMETER OutputFile
Filename for saving the site list (.json or .csv). Defaults to 'datto_sites.csv'.
.PARAMETER PushSiteVars
Writes fetched site variables into HKLM:\Software\SVS\Deployment.
.PARAMETER InstallRMM
Downloads and runs the Datto RMM agent installer.
.PARAMETER SaveCopy
Saves a copy of the downloaded agent installer to C:\Temp.
.PARAMETER SiteUID
Unique identifier of the Datto site (required for install and registry push).
.PARAMETER SiteName
Friendly name of the Datto site (used for logging).
.EXAMPLE
# Fetch and save site list via webhook
Install-DattoRMM -UseWebhook -WebhookPassword 'Tndmeeisdwge!' -FetchSites -SaveSitesList -OutputFile 'sites.csv'
.EXAMPLE
# Headless install with site variables
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
-SiteUID 'site-123' -SiteName 'Acme Corp' -PushSiteVars -InstallRMM
.EXAMPLE
# Download and save installer to C:\Temp without installing
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
-SiteUID 'site-123' -SiteName 'Acme Corp' -SaveCopy
#>
function Install-DattoRMM {
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
param (
[switch]$UseWebhook,
[string]$WebhookPassword,
[string]$WebhookUrl = $Global:DattoWebhookUrl,
[string]$ApiUrl,
[string]$ApiKey,
[string]$ApiSecretKey,
[switch]$FetchSites,
[switch]$SaveSitesList,
[string]$OutputFile = 'datto_sites.csv',
[switch]$PushSiteVars,
[switch]$InstallRMM,
[switch]$SaveCopy,
[string]$SiteUID,
[string]$SiteName
)
# Validate mutually-dependent switches
if ($SaveSitesList -and -not $FetchSites) {
Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM; return
}
# 1) Optionally fetch credentials from webhook
if ($UseWebhook) {
if (-not $WebhookPassword) {
Write-LogHybrid "Webhook password missing." Error DattoRMM; return
}
try {
$resp = Invoke-RestMethod -Uri $WebhookUrl `
-Headers @{ SVSMSPKit = $WebhookPassword } `
-Method GET
$ApiUrl = $resp.ApiUrl
$ApiKey = $resp.ApiKey
$ApiSecretKey = $resp.ApiSecretKey
Write-LogHybrid "Webhook credentials fetched." Success DattoRMM
} catch {
Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM; return
}
}
# 2) Validate API parameters
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
Write-LogHybrid "Missing required API parameters." Error DattoRMM; return
}
# 3) Acquire OAuth token
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
try {
$publicCred = New-Object System.Management.Automation.PSCredential(
'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force)
)
$tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" `
-Credential $publicCred `
-Method Post `
-ContentType 'application/x-www-form-urlencoded' `
-Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey"
$token = $tokenResp.access_token
Write-LogHybrid "OAuth token acquired." Success DattoRMM
} catch {
Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM; return
}
$headers = @{ Authorization = "Bearer $token" }
# 4) Fetch site list only
if ($FetchSites) {
try {
$sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers
$siteList = $sitesResp.sites | ForEach-Object {
[PSCustomObject]@{ Name = $_.name; UID = $_.uid }
}
Write-LogHybrid "Fetched $($siteList.Count) sites." Success DattoRMM
if ($SaveSitesList) {
$desktop = [Environment]::GetFolderPath('Desktop')
$path = Join-Path $desktop $OutputFile
$ext = [IO.Path]::GetExtension($OutputFile).ToLower()
if ($ext -eq '.json') {
$siteList | ConvertTo-Json -Depth 3 | Out-File -FilePath $path -Encoding UTF8
} else {
$siteList | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
}
Write-LogHybrid "Wrote $($siteList.Count) sites to $path" Success DattoRMM
}
return $siteList
} catch {
Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM; return @()
}
}
# 5) Push site variables to registry
if ($PushSiteVars) {
try {
$varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers
Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM
} catch {
Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM
}
$regPath = "HKLM:\Software\SVS\Deployment"
foreach ($v in $varsResp.variables) {
try {
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null
Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM
} catch {
Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM
}
}
}
# 6) Download & install RMM agent
if ($InstallRMM) {
if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) {
try {
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
$tmp = "$env:TEMP\AgentInstall.exe"
Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing
Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM
Start-Process -FilePath $tmp -NoNewWindow
Write-LogHybrid "RMM agent installer launched." Success DattoRMM
} catch {
Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM
}
}
}
# 7) Save a copy of installer to C:\Temp
if ($SaveCopy) {
try {
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
$path = "C:\Temp\AgentInstall.exe"
if (-not (Test-Path 'C:\Temp')) { New-Item -Path 'C:\Temp' -ItemType Directory | Out-Null }
Invoke-WebRequest -Uri $dlUrl -OutFile $path -UseBasicParsing
Write-LogHybrid "Saved installer copy to $path." Info DattoRMM
} catch {
Write-LogHybrid "Save-copy failed: $($_.Exception.Message)" Error DattoRMM
}
}
# 8) Warn if no action was taken
if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning DattoRMM
}
}
#endregion Install-DattoRMM
#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 = Install-DattoRMM `
-UseWebhook `
-WebhookPassword $WebhookPassword `
-FetchSites `
-SaveSitesList:$SaveSitesList `
-OutputFile $OutputFile
Write-LogHybrid "Done." 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 `
-UseWebhook `
-WebhookPassword $WebhookPassword `
-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 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
}
}