#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 <# .SYNOPSIS ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface, and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment. .DESCRIPTION Provides an HTTP-hosted GUI for selecting and running tasks (module installs, tweaks, offboarding, etc.). When invoked with the correct parameters, it can silently install the SVSMSP toolkit and perform a headless DattoRMM deployment without ever launching the browser or UI. .PARAMETER SilentInstall Runs only the SVSMSP module install (Install-Toolkit) and skips launching the browser/UI. .PARAMETER DattoApiUrl The Datto Automate API base URL for headless deployment. .PARAMETER DattoApiKey Your Datto Automate API username. .PARAMETER DattoApiSecretKey Your Datto Automate API password/secret. .PARAMETER SiteUID The target Datto site UID for headless installation. .PARAMETER SiteName The target Datto site name for headless installation. .PARAMETER PushSiteVars Switch to include site variables in the headless DattoRMM install. .PARAMETER InstallRMM Switch to install the RMM agent in the headless DattoRMM install. .PARAMETER SaveCopy Switch to download the RMM installer executable during the headless DattoRMM install. .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -N8nPassword 'pwd' ` -SiteUID 'site-123' ` -SiteName 'Acme Corp' ` -InstallRMM ` -PushSiteVars ` -SaveCopy ` -WhatIf .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content )) ` -N8nPassword 's3cr3t' ` -FetchSitesOnly ` -OutputFile 'sites.json' .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com/ScriptMonkey.ps1' -UseBasicParsing).Content )) ` -N8nPassword 's3cr3t' ` -FetchSitesOnly # β writes datto_sites.csv .EXAMPLE & ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall .EXAMPLE & ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup .EXAMPLE not tested but i thin this is how would call it iex (iwr 'https://your.server/ScriptMonkey.ps1' -UseBasicParsing).Content; Invoke-ScriptMonkey -DattoApiUrl 'https://β¦' -DattoApiKey 'β¦' -DattoApiSecretKey 'β¦' -SiteUID 'β¦' -SiteName 'β¦' -InstallRMM -PushSiteVars" #> #region ScriptMonkey run silently Entrypoint # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # 1) ENTRYPOINT + PARAMETER DECLARATION # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ function Invoke-ScriptMonkey { [CmdletBinding( DefaultParameterSetName='UI', SupportsShouldProcess=$true, ConfirmImpact= 'Medium' )] param( # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Toolkit-only mode [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # remove Toolkit [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Datto headless mode # Both Datto sets share the webhook password # Shared webhook password for both Datto modes [Parameter(Mandatory,ParameterSetName='DattoFetch')] [Parameter(Mandatory,ParameterSetName='DattoInstall')] [string]$N8nPassword, # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Fetch only set write sites and exit [Parameter(ParameterSetName='DattoFetch')][switch] $FetchSitesOnly, [Parameter(ParameterSetName='DattoFetch')][string] $OutputFile = 'datto_sites.csv', # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Install set: target site must be provided [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID, [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName, [Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars, [Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM, [Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy ) #region ScriptMonkey run silently Entrypoint # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # 3) MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI) # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ switch ($PSCmdlet.ParameterSetName) { 'Toolkit' { Write-LogHybrid "Toolkit-only mode" Info Startup Install-SVSMSP -InstallToolkit return } 'Cleanup' { Write-LogHybrid "Running Toolkit cleanup mode" Info Startup Install-SVSMSP -Cleanup return } # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # 2) If user only wants the site list, do that and exit # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 'DattoFetch' { Write-LogHybrid "Fetching site list onlyβ¦" Info DattoAuth $sites = Get-DattoRmmSites -Password $N8nPassword $ext = [IO.Path]::GetExtension($OutputFile).ToLower() if ($ext -eq '.json') { $sites | ConvertTo-Json -Depth 3 | Out-File -FilePath $OutputFile -Encoding UTF8 } else { $sites | Export-Csv -Path $OutputFile -NoTypeInformation -Encoding UTF8 } Write-LogHybrid "Wrote $($sites.Count) sites to $OutputFile" Success DattoAuth return } # ββββββββββββββββββββββββββββββββββββββββββββ # 3) Invoke the existing Install-DattoRMM cmdlet # ββββββββββββββββββββββββββββββββββββββββββββ 'DattoInstall' { Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) { Install-DattoRMM ` -ApiUrl $Global:ApiUrl ` -ApiKey $Global:ApiKey ` -ApiSecretKey $Global:ApiSecretKey ` -SiteUID $SiteUID ` -SiteName $SiteName ` -PushSiteVars:$PushSiteVars ` -InstallRMM:$InstallRMM ` -SaveCopy:$SaveCopy } return } 'UI' { Write-Host "Launching UI" Info Startup -ForegroundColor Cyan 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 ScriptMonkey run silently Entrypoint Write-Host "π οΈ SAMY - Script Automation Monkey (Yeah!)" -ForegroundColor Cyan Write-Host "ParameterSetName: $($PSCmdlet.ParameterSetName)" -ForegroundColor Yellow #region β guarantee NuGet provider is present without prompting # βββ Top of script βββ Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null # βββ ensure TLS 1.2 + no prompts βββ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' # check if NuGet exists (no outputβassigned to $nuget) $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue if (-not $nuget) { # install it (again, assignment suppresses the table) Install-PackageProvider ` -Name NuGet ` -MinimumVersion 2.8.5.201 ` -Force ` -Confirm:$false # re-query just for version info $found = Get-PackageProvider -Name NuGet -ListAvailable Write-Host "Installed NuGet provider v$($found.Version)" -ForegroundColor Green } else { Write-Host "NuGet provider already present (v$($found.Version))" -ForegroundColor DarkGray } # now import it silently Import-PackageProvider -Name NuGet -Force -ErrorAction SilentlyContinue | Out-Null # ensure trust PSGallery without its own output (so you don't get βuntrusted repositoryβ prompt $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($gallery.InstallationPolicy -ne 'Trusted') { Set-PSRepository ` -Name PSGallery ` -InstallationPolicy Trusted ` -ErrorAction SilentlyContinue | Out-Null Write-Host "PSGallery marked as Trusted" -ForegroundColor Green } #endregion # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # 2) GLOBAL SETTINGS & HELPERS # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ # Listening port for HTTP UI $Port = 8082 # Configurable endpoints $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' #region Get-DattoApiCredentials function Get-DattoApiCredentials { [CmdletBinding()] param ( [Parameter(Mandatory)][string]$Password ) $headers = @{ "SVSMSPKit" = $Password } try { $resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl ` -Headers $headers ` -Method GET return @{ ApiUrl = $resp.ApiUrl ApiKey = $resp.ApiKey ApiSecretKey = $resp.ApiSecretKey } } catch { Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" Error DattoAuth return $null } } function Get-DattoRmmSites { [CmdletBinding()] param( [Parameter(Mandatory)] [string] $Password, [Parameter()] [string] $WebhookUrl = $Global:DattoWebhookUrl ) # 1) Fetch Datto API credentials from your webhook Write-Verbose "Fetching Datto API credentials from $WebhookUrl" try { $headers = @{ 'SVSMSPKit' = $Password } $creds = Invoke-RestMethod -Uri $WebhookUrl -Headers $headers -Method GET } catch { Throw "Failed to fetch credentials from webhook: $_" } $apiUrl = $creds.ApiUrl $apiKey = $creds.ApiKey $apiSecretKey = $creds.ApiSecretKey # 2) Request an OAuth token Write-Verbose "Requesting OAuth token from $apiUrl/auth/oauth/token" try { $securePwd = ConvertTo-SecureString -String 'public' -AsPlainText -Force $credObj = New-Object System.Management.Automation.PSCredential('public-client', $securePwd) $tokenResp = Invoke-RestMethod ` -Uri "$apiUrl/auth/oauth/token" ` -Credential $credObj ` -Method 'POST' ` -ContentType 'application/x-www-form-urlencoded' ` -Body "grant_type=password&username=$apiKey&password=$apiSecretKey" $token = $tokenResp.access_token } catch { Throw "Failed to obtain OAuth token: $_" } # 3) Fetch the list of RMM sites Write-Verbose "Fetching RMM sites from $apiUrl/api/v2/account/sites" try { $authHeader = @{ Authorization = "Bearer $token" } $sitesResp = Invoke-RestMethod ` -Uri "$apiUrl/api/v2/account/sites" ` -Method 'GET' ` -Headers $authHeader ` -ContentType 'application/json' $siteList = $sitesResp.sites | Select-Object ` @{ Name = 'Name'; Expression = { $_.name } }, ` @{ Name = 'UID'; Expression = { $_.uid } } if (-not $siteList) { Write-Warning "No sites were returned by the API." return @() } return $siteList } catch { Throw "Failed to fetch sites from API: $_" } } # 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() } # Core Write-Log function (advanced with event-log support) 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) # βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ 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 #region Config & Task Definitions # 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 # If we got here, it's the UI setβlaunch browser + listener: # βββ UI fallback starts here βββ Write-LogHybrid "Launching UI" Info Startup #region Handler Stubs function Respond-Text { param($Context, $Text) $bytes = [Text.Encoding]::UTF8.GetBytes($Text) $Context.Response.ContentType = 'text/plain' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) $Context.Response.OutputStream.Close() } function Respond-HTML { [CmdletBinding()] param( [Parameter(Mandatory = $true)][object] $Context, [Parameter(Mandatory = $true)][string] $Html ) $bytes = [Text.Encoding]::UTF8.GetBytes($Html) $Context.Response.ContentType = 'text/html' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) $Context.Response.OutputStream.Close() } # new helper to return JSON function Respond-JSON { param($Context, $Object) $json = $Object | ConvertTo-Json -Depth 5 $bytes = [Text.Encoding]::UTF8.GetBytes($json) $Context.Response.ContentType = 'application/json' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) $Context.Response.OutputStream.Close() } #region Install-DattoRMM-Helper function Install-DattoRMM-Helper { param ( [string]$ApiUrl, [string]$ApiKey, [string]$ApiSecretKey, [switch]$FetchSitesOnly, [string]$SiteName, [string]$SiteUID ) if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) { Write-LogHybrid -Message "Missing required parameters. Please provide ApiUrl, ApiKey, and ApiSecretKey." -Level "Error" -LogToEvent return } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-LogHybrid -Message "Fetching OAuth token..." -Level "Info" try { $securePassword = ConvertTo-SecureString -String 'public' -AsPlainText -Force $apiGenToken = Invoke-WebRequest -Credential (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('public-client', $securePassword)) ` -Uri ('{0}/auth/oauth/token' -f $ApiUrl) ` -Method 'POST' ` -ContentType 'application/x-www-form-urlencoded' ` -Body ('grant_type=password&username={0}&password={1}' -f $ApiKey, $ApiSecretKey) ` | ConvertFrom-Json $requestToken = $apiGenToken.access_token Write-LogHybrid -Message "OAuth token fetched successfully." -Level "Success" -LogToEvent } catch { Write-LogHybrid -Message "Failed to fetch OAuth token. Details: $($_.Exception.Message)" -Level "Error" -LogToEvent return } $getHeaders = @{"Authorization" = "Bearer $requestToken"} if ($FetchSitesOnly) { Write-Host "Fetching list of sites from the Datto RMM API..." -ForegroundColor Cyan try { $getHeaders = @{"Authorization" = "Bearer $requestToken" } $getSites = Invoke-WebRequest -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $getHeaders -ContentType "application/json" $sitesJson = $getSites.Content | ConvertFrom-Json $siteList = $sitesJson.sites | ForEach-Object { [PSCustomObject]@{ Name = $_.name UID = $_.uid } } Write-Host "Successfully fetched list of sites." -ForegroundColor Green return $siteList } catch { Write-Host "Failed to fetch sites from the API. Details: $($_.Exception.Message)" -ForegroundColor Red return } } } #endregion #region 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 # POST /getpw β read JSON body, call helper, return JSON function Handle-FetchSites { param($Context) # 1) Read incoming JSON (using block auto-disposes the reader) $reader = [IO.StreamReader]::new($Context.Request.InputStream) try { $raw = $reader.ReadToEnd() } finally { $reader.Close() } try { $pw = (ConvertFrom-Json $raw).password } catch { Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites" returnRespondEmpty $Context return } # 2) Fetch your Datto API creds from the webhook Write-LogHybrid "Calling webhook for Datto credentialsβ¦" "Info" "FetchSites" try { $creds = Get-DattoApiCredentials -Password $pw if (-not $creds) { Write-LogHybrid "Webhook returned no credentials" Error FetchSites returnRespondEmpty $Context 403 return } # reuse the same globals from the entrypoint $Global:ApiUrl = $creds.ApiUrl $Global:ApiKey = $creds.ApiKey $Global:ApiSecretKey = $creds.ApiSecretKey Write-LogHybrid "Fetched and stored API credentials." Success FetchSites } catch { Write-LogHybrid "Credential-fetch error: $($_.Exception.Message)" Error FetchSites -LogToEvent returnRespondEmpty $Context 500 return } # 3) Exchange for a bearer token Write-LogHybrid "Requesting OAuth token" "Info" "FetchSites" try { $securePublic = ConvertTo-SecureString 'public' -AsPlainText -Force $creds = New-Object System.Management.Automation.PSCredential('public-client',$securePublic) $tokenResp = Invoke-RestMethod ` -Uri "$Global:ApiUrl/auth/oauth/token" ` -Credential $creds ` -Method Post ` -ContentType 'application/x-www-form-urlencoded' ` -Body "grant_type=password&username=$Global:ApiKey&password=$Global:ApiSecretKey" $token = $tokenResp.access_token Write-LogHybrid "OAuth token acquired." "Success" "FetchSites" } catch { Write-LogHybrid "OAuth request failed: $($_.Exception.Message)" "Error" "FetchSites" returnRespondEmpty $Context 500 return } # 4) Pull the site list Write-LogHybrid "Fetching Datto RMM site list" "Info" "FetchSites" try { $hdr = @{ Authorization = "Bearer $token" } $sitesResp = Invoke-RestMethod -Uri "$Global:ApiUrl/api/v2/account/sites" ` -Method Get ` -Headers $hdr ` -ContentType 'application/json' $siteList = $sitesResp.sites | ForEach-Object { [PSCustomObject]@{ Name = $_.name; UID = $_.uid } } Write-LogHybrid "Site list retrieved ($($siteList.Count) sites)." "Success" "FetchSites" } catch { Write-LogHybrid "Failed to fetch site list: $($_.Exception.Message)" "Error" "FetchSites" returnRespondEmpty $Context 500 return } # 5) Return JSON array $json = $siteList | ConvertTo-Json -Depth 2 $bytes = [Text.Encoding]::UTF8.GetBytes($json) $Context.Response.ContentType = 'application/json' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) $Context.Response.OutputStream.Close() } # Helper function to consistently return an empty JSON array function returnRespondEmpty { param( [Parameter(Mandatory)][object]$Context, [Parameter(Mandatory)][ValidateRange(100,599)][int]$StatusCode = 500 ) # Always return an empty JSON array body $empty = [Text.Encoding]::UTF8.GetBytes("[]") # Set the desired status code and headers $Context.Response.StatusCode = $StatusCode $Context.Response.ContentType = 'application/json' $Context.Response.ContentLength64 = $empty.Length # Write and close $Context.Response.OutputStream.Write($empty, 0, $empty.Length) $Context.Response.OutputStream.Close() } # On-boarding handlers function Handle-SetSVSPowerPlan { param($Context) # 1) call into your module Set-SVSPowerPlan # 2) log & write back a simple text response Write-LogHybrid "PowerPlan set" "Success" "OnBoard" Respond-Text $Context "PowerPlan applied" } function Handle-InstallSVSMSP { param($Context) Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" "Info" "OnBoard" try { Install-SVSMSP -InstallToolkit Respond-Text $Context "SVSMSP Module installed/updated." } catch { Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard" Respond-Text $Context "ERROR: $_" } } function Handle-InstallCyberQP { param($Context) # 1) call into your module Install-CyberQP # 2) log & write back a simple text response Write-LogHybrid "CyberQP installed" "Success" "OnBoard" Respond-Text $Context "CyberQP installed" } function Handle-InstallThreatLocker { param($Context) # 1) call into your module Install-ThreatLocker # 2) log & write back a simple text response Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard" Respond-Text $Context "ThreatLocker installed" } function Handle-InstallRocketCyber { param($Context) # 1) call into your module Install-RocketCyber # 2) log & write back a simple text response Write-LogHybrid "RocketCyber installed" "Success" "OnBoard" Respond-Text $Context "RocketCyber installed" } function Handle-InstallSVSHelpDesk { param($Context) # 1) call into your module Install-SVSHelpDesk # 2) log & write back a simple text response Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard" Respond-Text $Context "SVS HelpDesk installed" } function Handle-InstallDattoRMM { param($Context) $req = $Context.Request $resp = $Context.Response if ($req.HttpMethod -ne 'POST') { $resp.StatusCode = 405; $resp.ContentType = 'text/plain' $resp.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('Use POST'),0,7) $resp.OutputStream.Close(); return } # parse JSON body $body = (New-Object IO.StreamReader $req.InputStream).ReadToEnd() $data = $body | ConvertFrom-Json $checked = $data.checkedValues $uid = $data.UID $name = $data.Name try { Install-DattoRMM ` -ApiUrl $Global:ApiUrl ` -ApiKey $Global:ApiKey ` -ApiSecretKey $Global:ApiSecretKey ` -SiteUID $uid ` -SiteName $name ` -PushSiteVars:($checked -contains 'inputVar') ` -InstallRMM: ($checked -contains 'rmm') ` -SaveCopy: ($checked -contains 'exe') Write-LogHybrid "RMM install triggered for $name" "Success" "DattoRMM" $resp.StatusCode = 200 $responseString = "Triggered DattoRMM for $name" } catch { Write-LogHybrid "Error in Install-DattoRMM: $_" "Error" "DattoRMM" $resp.StatusCode = 500 $responseString = "ERROR: $($_.Exception.Message)" } $b = [Text.Encoding]::UTF8.GetBytes($responseString) $resp.ContentType = 'text/plain' $resp.ContentLength64 = $b.Length $resp.OutputStream.Write($b,0,$b.Length) $resp.OutputStream.Close() } # Off-boarding handlers function Handle-UninstallCyberQP { param($Context) # 1) call into your module Uninstall-CyberQP Write-LogHybrid "CyberQP uninstalled" "Success" "OffBoard" Respond-Text $Context "CyberQP uninstalled" } function Cleanup-SVSMSP { param($Context) Write-LogHybrid "SVSMSP cleaned up" "Success" "OffBoard" Respond-Text $Context "SVSMSP cleaned up" } # Tweaks handler function Disable-Animations { param($Context) Write-LogHybrid "Animations disabled" "Success" "Tweaks" Respond-Text $Context "Animations disabled" } # SVSApps handler function Install-WingetLastPass { param($Context) Write-LogHybrid "Winget LastPass installed" "Success" "SVSApps" Respond-Text $Context "Winget LastPass installed" } #endregion #region UI Generation 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 += @"
"@ } $html } ) -join "`n" } ### 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 "