diff --git a/StackMonkey_v1.ps1 b/StackMonkey_v1.ps1 deleted file mode 100644 index 42a0423..0000000 --- a/StackMonkey_v1.ps1 +++ /dev/null @@ -1,1677 +0,0 @@ -#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] $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 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 - - #region globalsetting - # ───────────────────────────────────────────────────────────────────────── - # 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 = "SVS Scripting", -, - [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, - [string]$EventSource = "Script Automation Monkey", - [string]$EventLog = "SVS Scripting" - ) - - 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 ` - -EventSource $EventSource ` - -EventLog $EventLog - } - else { - # Fall back to your helper - Write-LogHelper ` - -Message $Message ` - -Level $Level ` - -TaskCategory $TaskCategory ` - -LogToEvent:$LogToEvent ` - -EventSource $EventSource ` - -EventLog $EventLog - } -} - - - #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 - - - 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" - } - - - - ### 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') - - # - # 1) Inline your full original CSS here - # -$style = @' - -'@ - - -$script = @' - - -'@ - - # - # 3) The HTML skeleton with placeholders - # -$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 - - # 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 region globalsetting - - #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 - - 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 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 - - - - # 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 - - - - # POST /getpw β†’ read JSON body, call helper, return JSON - - - - - #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 - } -} - -