#region changes to be done
# We could change line 298 and 379 the have it log in "SVS Scripting" instead of "Application" if we can find a way to force create the log
# winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
# winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
#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
Install-DattoRMM is a single, unified toolkit for Datto RMM operations. It can be used
interactively or via HTTP endpoints, and includes built-in validation and error trapping.
Key features:
- Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook.
- OAuth management - automatically acquires and refreshes bearer tokens over TLS.
- Site list fetching - returns the list of RMM sites; validates OutputFile to .csv or .json.
- Site list saving - writes fetched site list to the user's Desktop as CSV or JSON.
- Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment.
- Agent download & install - downloads the Datto RMM agent installer and launches it.
- Installer archiving - saves a copy of the downloaded installer to C:\Temp.
- HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch
to log errors and return proper HTTP 500 responses on failure.
- Idempotent & WhatIf support - uses ShouldProcess for safe, testable agent installs.
Throughout, secrets are never written to logs or console, and all operations produce
clear success/failure messages via Write-LogHybrid.
#>
#region Safely bypass Restricted Execution Policy
# ─── Safely bypass Restricted Execution Policy ───
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or
(Get-ExecutionPolicy) -eq 'Restricted') {
Write-Host "[Info] Relaunching with ExecutionPolicy Bypass..." -ForegroundColor Yellow
if ($PSCommandPath) {
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`""
} else {
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }"
}
exit
}
# ─── TLS and silent install defaults ───
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ConfirmPreference = 'None'
#endregion Safely bypass Restricted Execution Policy
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$')][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
$Port = 8082
$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
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"
try {
Uninstall-Module -Name SVSMSP -AllVersions -Force -ErrorAction Stop
Write-LogHybrid "SVSMSP module uninstalled from system." "Success" "SVSModule" -LogToEvent
} catch {
if ($_.Exception.Message -match 'No match was found') {
Write-LogHybrid "No existing SVSMSP module found to uninstall." "Warning" "SVSModule" -LogToEvent
} else {
Write-LogHybrid "Failed to uninstall SVSMSP: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
if (Get-PSRepository -Name SVS_Repo -ErrorAction SilentlyContinue) {
try {
Unregister-PSRepository -Name SVS_Repo -ErrorAction Stop
Write-LogHybrid "SVS_Repo repository unregistered." "Success" "SVSModule" -LogToEvent
} catch {
Write-LogHybrid "Failed to unregister SVS_Repo: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
if (Get-Module -Name SVSMSP) {
try {
Remove-Module SVSMSP -Force -ErrorAction Stop
Write-LogHybrid "SVSMSP module removed from current session." "Success" "SVSModule" -LogToEvent
} catch {
Write-LogHybrid "Failed to remove SVSMSP from session: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
}
function Perform-ToolkitInstallation {
Perform-Cleanup
Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule" -LogToEvent
if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
}
Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule" -LogToEvent
Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force
Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" -LogToEvent
}
Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent
if ($Cleanup) { Perform-Cleanup; return }
if ($InstallToolkit) { Perform-ToolkitInstallation; return }
Perform-ToolkitInstallation
}
#endregion SVS Module
#region Write-Log
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 = "Script Automation Monkey",
[string]$EventLog = "SVS Scripting", # <— default changed from Application
[int] $CustomEventID,
[string]$LogFile,
[switch]$PassThru
)
$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
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
$Global:LogCache = [System.Collections.ArrayList]::new()
}
$Global:LogCache.Add([pscustomobject]@{
Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
Level = $Level
Message = $fmt
}) | Out-Null
if ($PSBoundParameters.LogFile) {
try {
"$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" | Out-File -FilePath $LogFile -Append -Encoding UTF8
} catch {
Write-Host "[Warning] File log failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
if ($LogToEvent) {
try {
# Ensure the log & source exist and are mapped together.
$regBase = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog'
$mappedLog = $null
if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
$mappedLog = (Get-ChildItem $regBase -ErrorAction SilentlyContinue |
Where-Object { Test-Path (Join-Path $_.PSPath $EventSource) } |
Select-Object -First 1
).PSChildName
}
if (-not $mappedLog) {
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
} elseif ($mappedLog -ne $EventLog) {
# Source already exists but mapped to a different log. Create a new source mapped to target log.
$newSource = "$EventSource - $EventLog"
if (-not [System.Diagnostics.EventLog]::SourceExists($newSource)) {
New-EventLog -LogName $EventLog -Source $newSource -ErrorAction Stop
}
$EventSource = $newSource
}
} catch {
Write-Host "[Warning] Could not ensure event log/source ($EventLog / $EventSource): $($_.Exception.Message)" -ForegroundColor Yellow
# continue; we'll still try to write
}
$entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' }
try {
Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $entryType -EventID $EventID -Message $fmt
} catch {
Write-Host "[Warning] EventLog write failed: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
if ($PassThru) { return $Global:LogCache[-1] }
}
function Write-LogHybrid {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)][string]$Message,
[ValidateSet("Info","Warning","Error","Success","General")]
[string]$Level = "Info",
[string]$TaskCategory = "GeneralTask",
[switch]$LogToEvent,
[string]$EventSource = "Script Automation Monkey",
[string]$EventLog = "SVS Scripting", # <— default changed from Application
[ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")]
[string]$ForegroundColorOverride
)
$formatted = "[$Level] [$TaskCategory] $Message"
if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) {
Write-Host $formatted -ForegroundColor $ForegroundColorOverride
$invokeParams = @{
Message = $Message
Level = $Level
TaskCategory = $TaskCategory
LogToEvent = $LogToEvent
EventSource = $EventSource
EventLog = $EventLog
}
if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } else { Write-LogHelper @invokeParams }
} else {
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog
} else {
Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog
}
}
}
#endregion Write-Log
#region building the Menus
$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' },
@{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' },
@{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; 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) {
$subHtml = (
$_.SubOptions | ForEach-Object {
""
}
) -join "`n"
$html += @"
$subHtml
"@
}
$html
}
) -join "`n"
}
#endregion Build-Checkboxes
#region Get-ModuleVersionHtml
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
function Get-NextFreePort {
param([int]$Start = $Port)
for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) {
$l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
try { $l.Start(); $l.Stop(); return $p } catch {}
}
throw "No free TCP port available."
}
# Starts the HTTP listener loop
function Start-Server {
$Global:Listener = [System.Net.HttpListener]::new()
$primaryPrefix = "http://localhost:$Port/"
$wildcardPrefix = "http://+:$Port/"
try {
$Global:Listener.Prefixes.Add($primaryPrefix)
$Global:Listener.Start()
Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent
}
catch [System.Net.HttpListenerException] {
if ($_.Exception.ErrorCode -eq 5) {
Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL…" Warning Server -LogToEvent
try {
$user = "$env:USERDOMAIN\$env:USERNAME"
if (-not $user.Trim()) { $user = $env:USERNAME }
Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait
$Global:Listener = [System.Net.HttpListener]::new()
$Global:Listener.Prefixes.Add($wildcardPrefix)
$Global:Listener.Start()
Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent
} catch {
Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent
return
}
}
elseif ($_.Exception.NativeErrorCode -in 32,183) {
$old = $Port
$Port = Get-NextFreePort -Start ($Port + 1)
$Global:Listener = [System.Net.HttpListener]::new()
$primaryPrefix = "http://localhost:$Port/"
$Global:Listener.Prefixes.Add($primaryPrefix)
$Global:Listener.Start()
Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent
}
else {
Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent
return
}
}
try {
while ($Global:Listener.IsListening) {
$ctx = $Global:Listener.GetContext()
try {
Dispatch-Request $ctx
} catch {
Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent
}
}
}
finally {
$Global:Listener.Close()
Write-LogHybrid "Listener closed." Info Server -LogToEvent
}
}
#endregion Strat-Server
#region UIHtml
function Get-UIHtml {
param([string]$Page = 'onboard')
$style = @'
'@
$script = @'
'@
$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
"@
# Build the checkbox HTML/JS from $Global:Tasks
$onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left'
$onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right'
$offboard = Build-Checkboxes -Page 'offboard' -Column ''
$tweaks = Build-Checkboxes -Page 'tweaks' -Column ''
$apps = Build-Checkboxes -Page 'SVSApps' -Column ''
$tasksJsAll = (
$Global:Tasks | ForEach-Object {
" { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
}
) -join ",`n"
$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 HTTP helpers + Handlers
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 {
$raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$pw = (ConvertFrom-Json $raw).password
$Global:WebhookPassword = $pw
$sites = Install-DattoRMM -UseWebhook -WebhookPassword $pw -FetchSites
Respond-JSON $Context $sites
} catch {
Write-LogHybrid "Handle-FetchSites error: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
$Context.Response.StatusCode = 500
Respond-Text $Context "Internal server error fetching sites."
}
}
function Handle-SetSVSPowerPlan { param($Context)
Set-SVSPowerPlan
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: $($_.Exception.Message)" "Error" "OnBoard"
Respond-Text $Context "ERROR: $($_.Exception.Message)"
}
}
function Handle-InstallCyberQP { param($Context)
Install-CyberQP
Write-LogHybrid "CyberQP installed" "Success" "OnBoard"
Respond-Text $Context "CyberQP installed"
}
function Handle-InstallThreatLocker { param($Context)
Install-ThreatLocker
Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard"
Respond-Text $Context "ThreatLocker installed"
}
function Handle-InstallRocketCyber { param($Context)
Install-RocketCyber
Write-LogHybrid "RocketCyber installed" "Success" "OnBoard"
Respond-Text $Context "RocketCyber installed"
}
function Handle-InstallSVSHelpDesk { param($Context)
Install-SVSHelpDesk
Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard"
Respond-Text $Context "SVS HelpDesk installed"
}
# NEW: Winget app handlers
function Handle-InstallChrome { param($Context)
try {
winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
Write-LogHybrid "Installed Google Chrome via winget" "Success" "SVSApps" -LogToEvent
Respond-Text $Context "Chrome installed"
} catch {
Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" "Error" "SVSApps" -LogToEvent
Respond-Text $Context "ERROR: $($_.Exception.Message)"
}
}
function Handle-InstallAcrobat { param($Context)
try {
winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
Write-LogHybrid "Installed Adobe Acrobat Reader (64-bit) via winget" "Success" "SVSApps" -LogToEvent
Respond-Text $Context "Acrobat Reader installed"
} catch {
Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" "Error" "SVSApps" -LogToEvent
Respond-Text $Context "ERROR: $($_.Exception.Message)"
}
}
function Handle-InstallDattoRMM {
param($Context)
try {
if ($Context.Request.HttpMethod -ne 'POST') {
$Context.Response.StatusCode = 405
Respond-Text $Context 'Use POST'
return
}
$body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
$data = ConvertFrom-Json $body
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')
Respond-Text $Context "Triggered DattoRMM for $($data.Name)"
} catch {
Write-LogHybrid "Handle-InstallDattoRMM error: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
$Context.Response.StatusCode = 500
Respond-Text $Context "Internal server error during DattoRMM install."
}
}
#endregion HTTP helpers + Handlers
#region Install-DattoRMM
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
)
if ($SaveSitesList -and -not $FetchSites) {
Write-LogHybrid "-SaveSitesList requires -FetchSites." "Error" "DattoRMM" -LogToEvent
return
}
if ($UseWebhook) {
if (-not $WebhookPassword) {
Write-LogHybrid "Webhook password missing." "Error" "DattoRMM" -LogToEvent
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" -LogToEvent
} catch {
Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
return
}
}
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
Write-LogHybrid "Missing required API parameters." "Error" "DattoRMM" -LogToEvent
return
}
[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" -LogToEvent
} catch {
Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
return
}
$headers = @{ Authorization = "Bearer $token" }
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" -LogToEvent
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" -LogToEvent
}
return $siteList
} catch {
Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
return @()
}
}
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" -LogToEvent
} catch {
Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
}
$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" -LogToEvent
} catch {
Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
}
}
}
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" -LogToEvent
Start-Process -FilePath $tmp -NoNewWindow
Write-LogHybrid "RMM agent installer launched." "Success" "DattoRMM" -LogToEvent
} catch {
Write-LogHybrid "Agent install failed: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
}
}
}
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" -LogToEvent
} catch {
Write-LogHybrid "Save-copy failed: $($_.Exception.Message)" "Error" "DattoRMM" -LogToEvent
}
}
if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." "Warning" "DattoRMM" -LogToEvent
}
}
#endregion Install-DattoRMM
#region Dispatch-Request
function Dispatch-Request {
param($Context)
$path = $Context.Request.Url.AbsolutePath.TrimStart('/')
if ($path -eq 'quit') {
Write-LogHybrid "Shutdown requested" "Info" "Server" -LogToEvent
Respond-Text $Context "Server shutting down."
$Global:Listener.Stop()
return
}
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
Handle-FetchSites $Context
return
}
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 = $Global:Tasks | Where-Object Name -EQ $path
if ($task) { & $task.HandlerFn $Context; return }
$Context.Response.StatusCode = 404
Respond-Text $Context '404 - Not Found'
}
#endregion Dispatch-Request
#region MAIN MODE SWITCH
switch ($PSCmdlet.ParameterSetName) {
'Toolkit' {
Write-LogHybrid "Toolkit-only mode" "Info" "Startup" -LogToEvent
Install-SVSMSP -InstallToolkit
return
}
'Cleanup' {
Write-LogHybrid "Running Toolkit cleanup mode" "Info" "Startup" -LogToEvent
Install-SVSMSP -Cleanup
return
}
'DattoFetch' {
Write-LogHybrid "Fetching site list only…" "Info" "DattoAuth" -LogToEvent
$sites = Install-DattoRMM -UseWebhook -WebhookPassword $WebhookPassword -FetchSites -SaveSitesList:$SaveSitesList -OutputFile $OutputFile
Write-LogHybrid "Done." "Success" "DattoAuth" -LogToEvent
return
}
'DattoInstall' {
Write-LogHybrid "Headless DattoRMM deploy" "Info" "DattoAuth" -LogToEvent
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 "Starting ScriptMonkey UI on http://localhost:$Port/" Info Startup
try {
Start-Server
try {
Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port" -ErrorAction Stop
} catch {
Start-Process "http://localhost:$Port"
}
} catch {
Write-LogHybrid "Failed to start server: $($_.Exception.Message)" Error Startup -LogToEvent
}
return
}
}
#endregion MAIN MODE SWITCH
#region Bootstrap NuGet/PSGallery silently
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ConfirmPreference = 'None'
$provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies"
if (-not (Test-Path $provPath)) {
try {
New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
Write-LogHybrid "Created missing provider folder: $provPath" "Info" "Bootstrap" -LogToEvent
} catch {
Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" "Warning" "Bootstrap" -LogToEvent
}
}
if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) {
try {
Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-LogHybrid "Installed PowerShellGet module" "Info" "Bootstrap" -LogToEvent
} catch {
Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" "Error" "Bootstrap" -LogToEvent
}
}
$pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version
if ($pkgMgmtVersion -lt [Version]"1.3.1") {
try {
Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop
Write-LogHybrid "Updated PackageManagement to latest version" "Info" "Bootstrap" -LogToEvent
} catch {
Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" "Warning" "Bootstrap" -LogToEvent
}
}
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
try {
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
Write-LogHybrid "PSGallery marked as Trusted" "Info" "Bootstrap" -LogToEvent
} catch {
Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" "Warning" "Bootstrap" -LogToEvent
}
}
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
if (-not $nuget) {
try {
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" "Info" "Bootstrap" -LogToEvent
} catch {
Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" "Error" "Bootstrap" -LogToEvent
}
} else {
Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" "Info" "Bootstrap" -LogToEvent
}
try { Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null }
catch { Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" "Error" "Bootstrap" -LogToEvent }
#endregion Bootstrap NuGet/PSGallery silently
}
# end function Invoke-ScriptMonkey
# Entrypoint
if ($MyInvocation.InvocationName -eq '.') {
# dot-sourced, don't invoke
} elseif ($PSCommandPath) {
Invoke-ScriptMonkey @PSBoundParameters
} else {
if ($args.Count -gt 0) {
$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++ } else { $namedArgs[$key] = $true }
}
}
Invoke-ScriptMonkey @namedArgs
} else {
Invoke-ScriptMonkey
}
}