diff --git a/StackMonkey.ps1 b/StackMonkey.ps1 index 5b711d4..ece368f 100644 --- a/StackMonkey.ps1 +++ b/StackMonkey.ps1 @@ -87,1480 +87,1497 @@ # ───────────────────────────────────────────────────────────────────────── # 1) ENTRYPOINT + PARAMETER DECLARATION # ───────────────────────────────────────────────────────────────────────── - - [CmdletBinding( - DefaultParameterSetName='UI', - SupportsShouldProcess=$true, - ConfirmImpact= 'Medium' - )] - param( - # ───────────────────────────────────────────────────────── - # Toolkit-only mode - [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, - # ───────────────────────────────────────────────────────── - # Datto headless mode - - # Both Datto sets share the webhook password - # Shared webhook password for both Datto modes +function Invoke-ScriptMonkey { + [CmdletBinding( + DefaultParameterSetName='UI', + SupportsShouldProcess=$true, + ConfirmImpact='Medium' + )] + param( + [Parameter(Mandatory,ParameterSetName='Toolkit')] [switch]$SilentInstall, [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 — 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 + [string]$N8nPassword, + … ) - $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()] + # — all of your modules, helpers, functions, etc. go here — + + [CmdletBinding( + DefaultParameterSetName='UI', + SupportsShouldProcess=$true, + ConfirmImpact= 'Medium' + )] param( - [Parameter(Mandatory)] - [string] $Password, + # ───────────────────────────────────────────────────────── + # Toolkit-only mode + [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, + # ───────────────────────────────────────────────────────── + # Datto headless mode - [Parameter()] - [string] $WebhookUrl = $Global:DattoWebhookUrl + # 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 ) - # 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: $_" - } + #region — guarantee NuGet provider is present without prompting - $apiUrl = $creds.ApiUrl - $apiKey = $creds.ApiKey - $apiSecretKey = $creds.ApiSecretKey + # ─── Top of script ─── + Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null + Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null - # 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) + # ─── ensure TLS 1.2 + no prompts ─── + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $ProgressPreference = 'SilentlyContinue' + $ConfirmPreference = 'None' - $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" + # check if NuGet exists (no output—assigned to $nuget) + $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue - $token = $tokenResp.access_token - } - catch { - Throw "Failed to obtain OAuth token: $_" - } + if (-not $nuget) { + # install it (again, assignment suppresses the table) + Install-PackageProvider ` + -Name NuGet ` + -MinimumVersion 2.8.5.201 ` + -Force ` + -Confirm:$false - # 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' + + # 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 + } - $siteList = $sitesResp.sites | Select-Object ` - @{ Name = 'Name'; Expression = { $_.name } }, ` - @{ Name = 'UID'; Expression = { $_.uid } } + # now import it silently + Import-PackageProvider -Name NuGet -Force -ErrorAction SilentlyContinue | Out-Null - if (-not $siteList) { - Write-Warning "No sites were returned by the API." - return @() - } + # 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 - return $siteList - } - catch { - Throw "Failed to fetch sites from API: $_" - } -} + Write-Host "PSGallery marked as Trusted" -ForegroundColor Green + } + + #endregion -# 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() -} + # ───────────────────────────────────────────────────────────────────────── + # 2) GLOBAL SETTINGS & HELPERS + # ───────────────────────────────────────────────────────────────────────── -# Core Write-Log function (advanced with event-log support) - function Write-LogHelper { + # 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]$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 + [Parameter(Mandatory)] + [string] $Password, + + [Parameter()] + [string] $WebhookUrl = $Global:DattoWebhookUrl ) - # 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 } + # 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: $_" } - # 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 } + $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: $_" } - 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 - - -# STACK = Scripted Tooling for Automated Client Kickoff -# MONKEY = Module-based Onboarding & Next-step Kickoff Engine Yoke -# Conveys the idea of coupling tasks together and keeping them under control. - -#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 = " $($_.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
" -} - - -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}} -
-
-
- -
- -
-
-

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 - - - - -#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 -} - -# 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' -} - - -# Starts the HTTP listener loop -function Start-Server { - # make it accessible to Dispatch-Request - $Global:Listener = [System.Net.HttpListener]::new() - $Global:Listener.Prefixes.Add("http://localhost:$Port/") - $Global:Listener.Start() - Write-Host "Listening on http://localhost:$Port/ ..." - - try { - while ($Global:Listener.IsListening) { - $ctx = $Global:Listener.GetContext() - try { - Dispatch-Request $ctx - } catch { - Write-LogHybrid "Dispatch error: $_" "Error" "Server" - } - } - } finally { - # once the loop exits, clean up - $Global:Listener.Close() - Write-LogHybrid "Listener closed." "Info" "Server" - } -} - - -#endregion - -#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 + Write-LogHybrid "Fetched and stored API credentials." Success FetchSites + } catch { + Write-LogHybrid "Credential-fetch error: $($_.Exception.Message)" Error FetchSites -LogToEvent + returnRespondEmpty $Context 500 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")) { + + # 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 $SiteUID ` - -SiteName $SiteName ` - -PushSiteVars:$PushSiteVars ` - -InstallRMM:$InstallRMM ` - -SaveCopy:$SaveCopy + -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)" } - return + $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 = " $($_.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
" + } + + + 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}} +
+
+
+ +
+ +
+
+

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 + + + + + #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 + } + + # 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' + } + + + # Starts the HTTP listener loop + function Start-Server { + # make it accessible to Dispatch-Request + $Global:Listener = [System.Net.HttpListener]::new() + $Global:Listener.Prefixes.Add("http://localhost:$Port/") + $Global:Listener.Start() + Write-Host "Listening on http://localhost:$Port/ ..." + + try { + while ($Global:Listener.IsListening) { + $ctx = $Global:Listener.GetContext() + try { + Dispatch-Request $ctx + } catch { + Write-LogHybrid "Dispatch error: $_" "Error" "Server" + } + } + } finally { + # once the loop exits, clean up + $Global:Listener.Close() + Write-LogHybrid "Listener closed." "Info" "Server" + } + } + + + #endregion + + #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 + } + + # ─────────────────────────────────────────────────────────── + # 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 } - '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 - } + # ──────────────────────────────────────────── + # 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 + } - } - #endregion ScriptMonkey run silently Entrypoint + return -Invoke-ScriptMonkey @PSBoundParameters + } + + '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 +} # <— end of Invoke-ScriptMonkey + + + Invoke-ScriptMonkey @PSBoundParameters \ No newline at end of file