#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 = " $($_.Label)" if ($_.SubOptions) { # join inside the code block is fine $subHtml = ( $_.SubOptions | ForEach-Object { "" } ) -join "`n" $html += @" "@ } $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
SVS Logo {{moduleVersion}}
SAMY Logo
Script Automation Monkey (Yeah!)

On-Boarding

This new deployment method ensures everything is successfully deployed with greater ease!

SVSMSP Stack

{{onboardLeftColumn}}

Optional

{{onboardRightColumn}}

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 } }