diff --git a/StackMonkey_Beta.ps1 b/StackMonkey_Beta.ps1 new file mode 100644 index 0000000..44a340e --- /dev/null +++ b/StackMonkey_Beta.ps1 @@ -0,0 +1,1648 @@ +#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 '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" + +#> + + function Invoke-ScriptMonkey { + + # ───────────────────────────────────────────────────────────────────────── + # PARAMETERS + GLOBAL VARIABLES + # ───────────────────────────────────────────────────────────────────────── + + [CmdletBinding( + DefaultParameterSetName='UI', + SupportsShouldProcess=$true, + ConfirmImpact= 'Medium' + )] + param( + # ───────────────────────────────────────────────────────── + # Toolkit-only mode + [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, + + # ───────────────────────────────────────────────────────── + # remove Toolkit + [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, + + # ───────────────────────────────────────────────────────── + # Datto headless mode + + # Both Datto sets share the webhook password + # Shared webhook password for both Datto modes + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [string]$N8nPassword, + + # ───────────────────────────────────────────────────────── + # Fetch only set write sites and exit + [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesOnly, + [Parameter(ParameterSetName='DattoFetch')][string] $OutputFile = 'datto_sites.csv', + + # ───────────────────────────────────────────────────────── + # Install set: target site must be provided + [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID, + [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName, + [Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars, + [Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM, + [Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy + ) + + #region global variables + + # Listening port for HTTP UI + $Port = 8082 + + # Configurable endpoints + $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' + + # Initialize a global in-memory log cache + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) + { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + + #endregion global variables + + #region SVS Module + + function Install-SVSMSP { + param ( + [switch] $Cleanup, + [switch] $InstallToolkit, + [Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }), + [Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }), + [Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP", + [Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo", + [Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/" + ) + + function Perform-Cleanup { + Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule" + # …your old cleanup logic here… + } + + function Perform-ToolkitInstallation { + Perform-Cleanup + Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule" + if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) { + Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted + } + Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule" + Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force + Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" + } + + Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" + if ($Cleanup) { + Perform-Cleanup; return + } + if ($InstallToolkit) { + Perform-ToolkitInstallation; return + } + # default if no switch passed: + Perform-ToolkitInstallation + } + + #endregion SVS Module + + #region Get-DattoApiCredentials + function Get-DattoApiCredentials { + [CmdletBinding()] + param ( + [Parameter(Mandatory)][string]$Password + ) + $headers = @{ "SVSMSPKit" = $Password } + try { + $resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl ` + -Headers $headers ` + -Method GET + return @{ + ApiUrl = $resp.ApiUrl + ApiKey = $resp.ApiKey + ApiSecretKey = $resp.ApiSecretKey + } + } + catch { + Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" Error DattoAuth + return $null + } + } + + #endregion Get-DattoApiCredentials + + #region Get-DattoRMMSites + function Get-DattoRmmSites { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string] $Password, + + [Parameter()] + [string] $WebhookUrl = $Global:DattoWebhookUrl + ) + + # 1) Fetch Datto API credentials from your webhook + Write-Verbose "Fetching Datto API credentials from $WebhookUrl" + try { + $headers = @{ 'SVSMSPKit' = $Password } + $creds = Invoke-RestMethod -Uri $WebhookUrl -Headers $headers -Method GET + } + catch { + Throw "Failed to fetch credentials from webhook: $_" + } + + $apiUrl = $creds.ApiUrl + $apiKey = $creds.ApiKey + $apiSecretKey = $creds.ApiSecretKey + + # 2) Request an OAuth token + Write-Verbose "Requesting OAuth token from $apiUrl/auth/oauth/token" + try { + $securePwd = ConvertTo-SecureString -String 'public' -AsPlainText -Force + $credObj = New-Object System.Management.Automation.PSCredential('public-client', $securePwd) + + $tokenResp = Invoke-RestMethod ` + -Uri "$apiUrl/auth/oauth/token" ` + -Credential $credObj ` + -Method 'POST' ` + -ContentType 'application/x-www-form-urlencoded' ` + -Body "grant_type=password&username=$apiKey&password=$apiSecretKey" + + $token = $tokenResp.access_token + } + catch { + Throw "Failed to obtain OAuth token: $_" + } + + # 3) Fetch the list of RMM sites + Write-Verbose "Fetching RMM sites from $apiUrl/api/v2/account/sites" + try { + $authHeader = @{ Authorization = "Bearer $token" } + $sitesResp = Invoke-RestMethod ` + -Uri "$apiUrl/api/v2/account/sites" ` + -Method 'GET' ` + -Headers $authHeader ` + -ContentType 'application/json' + + $siteList = $sitesResp.sites | Select-Object ` + @{ Name = 'Name'; Expression = { $_.name } }, ` + @{ Name = 'UID'; Expression = { $_.uid } } + + if (-not $siteList) { + Write-Warning "No sites were returned by the API." + return @() + } + + return $siteList + } + catch { + Throw "Failed to fetch sites from API: $_" + } + } + #endregion Get-DattoRMMSites + + #region Write-Log + + # This function is used as a fallback if the SVSMSP module is not installed + function Write-LogHelper { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")][string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, [string]$EventSource="SVSMSP_Module", [string]$EventLog="Application", + [int]$CustomEventID, [string]$LogFile, [switch]$PassThru + ) + # IDs & colors + $idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 } + $colMap= @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" } + $EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] } + $color = $colMap[$Level] + $fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" + Write-Host $fmt -ForegroundColor $color + + # cache + if (-not $Global:LogCache) { $Global:LogCache = @() } + $entry = [pscustomobject]@{ Timestamp=(Get-Date -Format "yyyy-MM-dd HH:mm:ss"); Level=$Level; Message=$fmt } + $Global:LogCache += $entry + + # file + if ($PSBoundParameters.LogFile) { + try { "$($entry.Timestamp) $fmt" | Out-File $LogFile -Append -Encoding UTF8 } + catch { Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow } + } + + # event log + if ($LogToEvent) { + $etype = if ($Level -in 'Warning','Error') { $Level } else { 'Information' } + try { + if (-not (Get-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue)) { + New-EventLog -LogName $EventLog -Source $EventSource + } + $msg = "TaskCategory:$TaskCategory | Message:$Message" + Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $etype -EventID $EventID -Message $msg + } catch { Write-Host "[Warning] EventLog failed: $_" -ForegroundColor Yellow } + } + + if ($PassThru) { return $entry } + } + + # ───────────────────────────────────────────────────────────────────────── + # WRITE-LOG HYBRID (single definition, chooses at runtime if we use the + # Write-Log from the module or the built-in Write-LogHelper funtions ) + # ───────────────────────────────────────────────────────────────────────── + + function Write-LogHybrid { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent + ) + + if ( Get-Command -Name Write-Log -ErrorAction SilentlyContinue ) { + # SVSMSP module's Write-Log is available + Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent + } + else { + # fall back to your helper + Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent + } + } + + + #endregion Write-Log + + #region building the Menus + + # Define every task once here: + # Id β†’ checkbox HTML `id` + # Name β†’ URL path (`/Name`) + # Label β†’ user-visible text + # HandlerFn β†’ the PowerShell function to invoke + # Page β†’ which tab/page it appears on + + $Global:Tasks = @( + # On-Boarding, left column + @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' }, + @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' }, + @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' }, + @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' }, + @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' }, + @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' }, + @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left'; + SubOptions= @( + @{ Value='inputVar'; Label='Copy Site Variables' }, + @{ Value='rmm'; Label='Install RMM Agent' }, + @{ Value='exe'; Label='Download Executable' } + ) + }, + + + # On-Boarding, right column (optional bits) + @{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' }, + @{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' }, + + # Off-Boarding + @{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' }, + @{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' }, + + # Tweaks + @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' }, + + # SVS Apps + @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' } + ) + + #endregion building the Menus + + #region Build-Checkboxes + function Build-Checkboxes { + param($Page, $Column) + + ( + $Global:Tasks | + Where-Object Page -EQ $Page | + Where-Object Column -EQ $Column | + ForEach-Object { + $taskId = $_.Id + $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) { + " title='$($_.Tooltip)'" + } else { '' } + + $html = " $($_.Label)" + + if ($_.SubOptions) { + # join inside the code block is fine + $subHtml = ( + $_.SubOptions | + ForEach-Object { + "" + } + ) -join "`n" + + $html += @" + +"@ + } + + $html + } + ) -join "`n" + } + + #endregion Build-Checkboxes + + ### 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
" + } + + # 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" + } + } + + #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() + } + + # new helper to return JSON + function Respond-JSON { + param($Context, $Object) + $json = $Object | ConvertTo-Json -Depth 5 + $bytes = [Text.Encoding]::UTF8.GetBytes($json) + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) + $Context.Response.OutputStream.Close() + } + + function Handle-FetchSites { + param($Context) + + # 1) Read incoming JSON (using block auto-disposes the reader) + + $reader = [IO.StreamReader]::new($Context.Request.InputStream) + try { + $raw = $reader.ReadToEnd() + } finally { + $reader.Close() + } + + try { + $pw = (ConvertFrom-Json $raw).password + } catch { + Write-LogHybrid "Invalid JSON in /getpw payload: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context + return + } + + # 2) Fetch your Datto API creds from the webhook + Write-LogHybrid "Calling webhook for Datto credentials…" "Info" "FetchSites" + + try { + $creds = Get-DattoApiCredentials -Password $pw + if (-not $creds) { + Write-LogHybrid "Webhook returned no credentials" Error FetchSites + returnRespondEmpty $Context 403 + return + } + + # reuse the same globals from the entrypoint + $Global:ApiUrl = $creds.ApiUrl + $Global:ApiKey = $creds.ApiKey + $Global:ApiSecretKey = $creds.ApiSecretKey + + Write-LogHybrid "Fetched and stored API credentials." Success FetchSites + } catch { + Write-LogHybrid "Credential-fetch error: $($_.Exception.Message)" Error FetchSites -LogToEvent + returnRespondEmpty $Context 500 + return + } + + + # 3) Exchange for a bearer token + Write-LogHybrid "Requesting OAuth token" "Info" "FetchSites" + try { + $securePublic = ConvertTo-SecureString 'public' -AsPlainText -Force + $creds = New-Object System.Management.Automation.PSCredential('public-client',$securePublic) + $tokenResp = Invoke-RestMethod ` + -Uri "$Global:ApiUrl/auth/oauth/token" ` + -Credential $creds ` + -Method Post ` + -ContentType 'application/x-www-form-urlencoded' ` + -Body "grant_type=password&username=$Global:ApiKey&password=$Global:ApiSecretKey" + $token = $tokenResp.access_token + Write-LogHybrid "OAuth token acquired." "Success" "FetchSites" + } catch { + Write-LogHybrid "OAuth request failed: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context 500 + return + } + + # 4) Pull the site list + Write-LogHybrid "Fetching Datto RMM site list" "Info" "FetchSites" + try { + $hdr = @{ Authorization = "Bearer $token" } + $sitesResp = Invoke-RestMethod -Uri "$Global:ApiUrl/api/v2/account/sites" ` + -Method Get ` + -Headers $hdr ` + -ContentType 'application/json' + + $siteList = $sitesResp.sites | ForEach-Object { + [PSCustomObject]@{ Name = $_.name; UID = $_.uid } + } + Write-LogHybrid "Site list retrieved ($($siteList.Count) sites)." "Success" "FetchSites" + } catch { + Write-LogHybrid "Failed to fetch site list: $($_.Exception.Message)" "Error" "FetchSites" + returnRespondEmpty $Context 500 + return + } + + # 5) Return JSON array + $json = $siteList | ConvertTo-Json -Depth 2 + $bytes = [Text.Encoding]::UTF8.GetBytes($json) + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $bytes.Length + $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) + $Context.Response.OutputStream.Close() + } + + + + # Helper function to consistently return an empty JSON array + function returnRespondEmpty { + param( + [Parameter(Mandatory)][object]$Context, + [Parameter(Mandatory)][ValidateRange(100,599)][int]$StatusCode = 500 + ) + # Always return an empty JSON array body + $empty = [Text.Encoding]::UTF8.GetBytes("[]") + + # Set the desired status code and headers + $Context.Response.StatusCode = $StatusCode + $Context.Response.ContentType = 'application/json' + $Context.Response.ContentLength64 = $empty.Length + + # Write and close + $Context.Response.OutputStream.Write($empty, 0, $empty.Length) + $Context.Response.OutputStream.Close() + } + + + + # On-boarding handlers + function Handle-SetSVSPowerPlan { + param($Context) + + # 1) call into your module + Set-SVSPowerPlan + + # 2) log & write back a simple text response + Write-LogHybrid "PowerPlan set" "Success" "OnBoard" + Respond-Text $Context "PowerPlan applied" + } + + function Handle-InstallSVSMSP { + param($Context) + Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" "Info" "OnBoard" + try { + Install-SVSMSP -InstallToolkit + Respond-Text $Context "SVSMSP Module installed/updated." + } catch { + Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard" + Respond-Text $Context "ERROR: $_" + } + } + + function Handle-InstallCyberQP { + param($Context) + + # 1) call into your module + Install-CyberQP + + # 2) log & write back a simple text response + Write-LogHybrid "CyberQP installed" "Success" "OnBoard" + Respond-Text $Context "CyberQP installed" + } + + function Handle-InstallThreatLocker { + param($Context) + + # 1) call into your module + Install-ThreatLocker + + # 2) log & write back a simple text response + Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard" + Respond-Text $Context "ThreatLocker installed" + } + + function Handle-InstallRocketCyber { + param($Context) + + # 1) call into your module + Install-RocketCyber + + # 2) log & write back a simple text response + Write-LogHybrid "RocketCyber installed" "Success" "OnBoard" + Respond-Text $Context "RocketCyber installed" + } + + function Handle-InstallSVSHelpDesk { + param($Context) + + # 1) call into your module + Install-SVSHelpDesk + + # 2) log & write back a simple text response + Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard" + Respond-Text $Context "SVS HelpDesk installed" + } + + function Handle-InstallDattoRMM { + param($Context) + $req = $Context.Request + $resp = $Context.Response + + if ($req.HttpMethod -ne 'POST') { + $resp.StatusCode = 405; $resp.ContentType = 'text/plain' + $resp.OutputStream.Write([Text.Encoding]::UTF8.GetBytes('Use POST'),0,7) + $resp.OutputStream.Close(); return + } + + # parse JSON body + $body = (New-Object IO.StreamReader $req.InputStream).ReadToEnd() + $data = $body | ConvertFrom-Json + $checked = $data.checkedValues + $uid = $data.UID + $name = $data.Name + + try { + Install-DattoRMM ` + -ApiUrl $Global:ApiUrl ` + -ApiKey $Global:ApiKey ` + -ApiSecretKey $Global:ApiSecretKey ` + -SiteUID $uid ` + -SiteName $name ` + -PushSiteVars:($checked -contains 'inputVar') ` + -InstallRMM: ($checked -contains 'rmm') ` + -SaveCopy: ($checked -contains 'exe') + + Write-LogHybrid "RMM install triggered for $name" "Success" "DattoRMM" + $resp.StatusCode = 200 + $responseString = "Triggered DattoRMM for $name" + } + catch { + Write-LogHybrid "Error in Install-DattoRMM: $_" "Error" "DattoRMM" + $resp.StatusCode = 500 + $responseString = "ERROR: $($_.Exception.Message)" + } + + $b = [Text.Encoding]::UTF8.GetBytes($responseString) + $resp.ContentType = 'text/plain' + $resp.ContentLength64 = $b.Length + $resp.OutputStream.Write($b,0,$b.Length) + $resp.OutputStream.Close() + } + + + # Off-boarding handlers + function Handle-UninstallCyberQP { + param($Context) + + # 1) call into your module + Uninstall-CyberQP + + Write-LogHybrid "CyberQP uninstalled" "Success" "OffBoard" + Respond-Text $Context "CyberQP uninstalled" + } + + function Cleanup-SVSMSP { + param($Context) + Write-LogHybrid "SVSMSP cleaned up" "Success" "OffBoard" + Respond-Text $Context "SVSMSP cleaned up" + } + + # Tweaks handler + function Disable-Animations { + param($Context) + Write-LogHybrid "Animations disabled" "Success" "Tweaks" + Respond-Text $Context "Animations disabled" + } + + # SVSApps handler + function Install-WingetLastPass { + param($Context) + Write-LogHybrid "Winget LastPass installed" "Success" "SVSApps" + Respond-Text $Context "Winget LastPass installed" + } + + #endregion Handler Stubs + + # 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' + } + + #region EntryPoint: Define Invoke-ScriptMonkey + + # ───────────────────────────────────────────────────────────────────────── + # 3) MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI) + # ───────────────────────────────────────────────────────────────────────── + + switch ($PSCmdlet.ParameterSetName) { + 'Toolkit' { + Write-LogHybrid "Toolkit-only mode" Info Startup + Install-SVSMSP -InstallToolkit + return + } + + 'Cleanup' { + Write-LogHybrid "Running Toolkit cleanup mode" Info Startup + Install-SVSMSP -Cleanup + return + } + + # ─────────────────────────────────────────────────────────── + # 2) If user only wants the site list, do that and exit + # ─────────────────────────────────────────────────────────── + + 'DattoFetch' { + Write-LogHybrid "Fetching site list only…" Info DattoAuth + $sites = Get-DattoRmmSites -Password $N8nPassword + + if ($SaveSitesOnly) { + # If SaveSitesOnly is true, save the output to a file on desktop + $desktopPath = [System.Environment]::GetFolderPath('Desktop') + $filePath = Join-Path -Path $desktopPath -ChildPath $OutputFile + + $ext = [IO.Path]::GetExtension($OutputFile).ToLower() + if ($ext -eq '.json') { + # Save the file to the desktop using the full path + $sites | ConvertTo-Json -Depth 3 | Out-File -FilePath $filePath -Encoding UTF8 + } else { + # Save the file to the desktop using the full path + $sites | Export-Csv -Path $filePath -NoTypeInformation -Encoding UTF8 + } + + Write-LogHybrid "Wrote $($sites.Count) sites to $filePath" Success DattoAuth + + } else { + # If SaveSitesOnly is not true, just fetch sites (for UI purposes or silent mode without saving) + Write-LogHybrid "Sites fetched successfully, but not saved." Success DattoAuth + } + return + } + + + # ──────────────────────────────────────────── + # 3) Invoke the existing Install-DattoRMM cmdlet + # ──────────────────────────────────────────── + + 'DattoInstall' { + Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth + if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) { + Install-DattoRMM ` + -ApiUrl $Global:ApiUrl ` + -ApiKey $Global:ApiKey ` + -ApiSecretKey $Global:ApiSecretKey ` + -SiteUID $SiteUID ` + -SiteName $SiteName ` + -PushSiteVars:$PushSiteVars ` + -InstallRMM:$InstallRMM ` + -SaveCopy:$SaveCopy + } + + return + + } + + 'UI' { + Write-LogHybrid "Launching UI" Info Startup + Write-Host "Starting ScriptMonkey UI on http://localhost:$Port/" -ForegroundColor Cyan + Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port" + Start-Server # blocks until you click Exit + return + } + + } + #endregion EntryPoint: Define Invoke-ScriptMonkey + + 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 + + # If we got here, it's the UI setβ€”launch browser + listener: + # β€”β€”β€” UI fallback starts here β€”β€”β€” + Write-LogHybrid "Launching UI" Info Startup + + #region Install-DattoRMM-Helper + function Install-DattoRMM-Helper { + param ( + [string]$ApiUrl, + [string]$ApiKey, + [string]$ApiSecretKey, + [switch]$FetchSitesOnly, + [string]$SiteName, + [string]$SiteUID + ) + if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) { + Write-LogHybrid -Message "Missing required parameters. Please provide ApiUrl, ApiKey, and ApiSecretKey." -Level "Error" -LogToEvent + return + } + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + Write-LogHybrid -Message "Fetching OAuth token..." -Level "Info" + try { + $securePassword = ConvertTo-SecureString -String 'public' -AsPlainText -Force + $apiGenToken = Invoke-WebRequest -Credential (New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList ('public-client', $securePassword)) ` + -Uri ('{0}/auth/oauth/token' -f $ApiUrl) ` + -Method 'POST' ` + -ContentType 'application/x-www-form-urlencoded' ` + -Body ('grant_type=password&username={0}&password={1}' -f $ApiKey, $ApiSecretKey) ` + | ConvertFrom-Json + $requestToken = $apiGenToken.access_token + Write-LogHybrid -Message "OAuth token fetched successfully." -Level "Success" -LogToEvent + } catch { + Write-LogHybrid -Message "Failed to fetch OAuth token. Details: $($_.Exception.Message)" -Level "Error" -LogToEvent + return + } + $getHeaders = @{"Authorization" = "Bearer $requestToken"} + if ($FetchSitesOnly) { + Write-Host "Fetching list of sites from the Datto RMM API..." -ForegroundColor Cyan + try { + $getHeaders = @{"Authorization" = "Bearer $requestToken" } + $getSites = Invoke-WebRequest -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $getHeaders -ContentType "application/json" + $sitesJson = $getSites.Content | ConvertFrom-Json + $siteList = $sitesJson.sites | ForEach-Object { + [PSCustomObject]@{ + Name = $_.name + UID = $_.uid + } + } + Write-Host "Successfully fetched list of sites." -ForegroundColor Green + return $siteList + } + catch { + Write-Host "Failed to fetch sites from the API. Details: $($_.Exception.Message)" -ForegroundColor Red + return + } + } + } + #endregion + + #region HTTP Listener & Routing + + # Handle shutdown command + if ($path -eq 'quit') { + Write-LogHybrid "Shutdown requested" "Info" "Server" + Respond-Text $Context "Server shutting down." + # This will break out of the while loop in Start-Server + $Global:Listener.Stop() + return + } + + + #endregion + + + } + + if ($MyInvocation.InvocationName -eq '.') { + # dot-sourced, don't invoke +} elseif ($PSCommandPath) { + # script was saved and run directly + Invoke-ScriptMonkey @PSBoundParameters +} else { + # iwr | iex fallback + if ($args.Count -gt 0) { + # Convert -Param value -Switch into a hashtable for splatting + $namedArgs = @{} + for ($i = 0; $i -lt $args.Count; $i++) { + if ($args[$i] -is [string] -and $args[$i].StartsWith('-')) { + $key = $args[$i].TrimStart('-') + $next = $args[$i + 1] + if ($next -and ($next -notlike '-*')) { + $namedArgs[$key] = $next + $i++ # Skip next one, it's the value + } else { + $namedArgs[$key] = $true + } + } + } + Invoke-ScriptMonkey @namedArgs + } else { + Invoke-ScriptMonkey + } +} + +