Files
Logo/StackMonkey.ps1
2025-06-28 20:38:42 -04:00

1443 lines
50 KiB
PowerShell
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# region changes to be done
# if (Get-Command msedge.exe -ErrorAction SilentlyContinue) {
# Start-Process msedge.exe -ArgumentList "--app=http://localhost:$Port"
#} else {
# Start-Process "microsoft-edge:http://localhost:$Port"
#}
#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
Before i built the Invoke-scriptmonkey
& ([ScriptBlock]::Create( (iwr 'sm.svstools.ca').Content )) -SilentInstall
.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
# ─────────────────────────────────────────────────────────────────────────
[CmdletBinding(
DefaultParameterSetName='UI',
SupportsShouldProcess=$true,
ConfirmImpact= 'Medium'
)]
param(
# ─────────────────────────────────────────────────────────
# Toolkit-only mode
[Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall,
# ─────────────────────────────────────────────────────────
# Datto headless mode
[Parameter(Mandatory,ParameterSetName='Datto')][string] $N8nPassword,
[Parameter(Mandatory,ParameterSetName='Datto')][string] $SiteUID,
[Parameter(Mandatory,ParameterSetName='Datto')][string] $SiteName,
[Parameter(ParameterSetName='Datto')][switch] $PushSiteVars,
[Parameter(ParameterSetName='Datto')][switch] $InstallRMM,
[Parameter(ParameterSetName='Datto')][switch] $SaveCopy
)
#region — guarantee NuGet provider is present without prompting
# ─── Top of script ───
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue
# ─── 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)
$nuget = Install-PackageProvider `
-Name NuGet `
-MinimumVersion 2.8.5.201 `
-Force `
-Confirm:$false
Write-Host "✔ Installed NuGet provider v$($nuget.Version)" -ForegroundColor Green
}
else {
Write-Host " NuGet provider already present (v$($nuget.Version))" -ForegroundColor DarkGray
}
# import it so Register-PSRepository / Install-Module won't prompt
Import-PackageProvider -Name NuGet -Force -ErrorAction SilentlyContinue
# ensure PSGallery is trusted (so you don't get “untrusted repository” prompt
if ((Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue).InstallationPolicy -ne 'Trusted') {
Set-PSRepository `
-Name PSGallery `
-InstallationPolicy Trusted `
-ErrorAction SilentlyContinue
}
#endregion
# ─────────────────────────────────────────────────────────────────────────
# 2) GLOBAL SETTINGS & HELPERS
# ─────────────────────────────────────────────────────────────────────────
# Listening port for HTTP UI
$Port = 8082
# Configurable endpoints
$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
#region Get-DattoApiCredentials
function Get-DattoApiCredentials {
[CmdletBinding()]
param (
[Parameter(Mandatory)][string]$Password
)
$headers = @{ "SVSMSPKit" = $Password }
try {
$resp = Invoke-RestMethod -Uri $Global:DattoWebhookUrl `
-Headers $headers `
-Method GET
return @{
ApiUrl = $resp.ApiUrl
ApiKey = $resp.ApiKey
ApiSecretKey = $resp.ApiSecretKey
}
}
catch {
Write-LogHybrid "Failed to fetch API credentials: $($_.Exception.Message)" Error DattoAuth
return $null
}
}
#region Logging Helpers
# Initialize a global in-memory log cache
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
$Global:LogCache = [System.Collections.ArrayList]::new()
}
# Core Write-Log function (advanced with event-log support)
function Write-LogHelper {
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Message,
[ValidateSet("Info","Warning","Error","Success","General")]
[string]$Level = "Info",
[string]$TaskCategory = "GeneralTask",
[switch]$LogToEvent,
[string]$EventSource = "SVSMSP_Module",
[string]$EventLog = "Application",
[int]$CustomEventID
)
$EventID = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }[$Level]
#$Icon = @{Info=[System.Char]::ConvertFromUtf32(0x1F4CB);Warning=[char]0x26A0;Error=[char]0x274C;Success=[char]0x2705;General=[char]0x1F4E6}[$Level]
$logEntry = [PSCustomObject]@{
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
Level = $Level
Message = "$Icon [$Level] [$TaskCategory] $Message (EventID:$EventID)"
}
[void]$Global:LogCache.Add($logEntry)
if ($LogToEvent) {
try {
if (-not (Get-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue)) {
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction SilentlyContinue
}
Write-EventLog -LogName $EventLog -Source $EventSource `
-EntryType $Level -EventId $EventID `
-Message $Message
} catch {
Write-Host "$([System.Char]::ConvertFromUtf32(0x26A0))$([System.Char]::ConvertFromUtf32(0xFE0F)) [Warning] [EventLog] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow
}
}
}
# Hybrid wrapper: uses your module's Write-Log if available, else falls back
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
function Write-LogHybrid { param($Message,$Level,$TaskCategory,$LogToEvent) Write-Log @PSBoundParameters }
} else {
function Write-LogHybrid { param($Message,$Level,$TaskCategory,$LogToEvent) Write-LogHelper @PSBoundParameters }
}
#endregion
# ─────────────────────────────────────────────────────────────────────────
# 3) MAIN LOGIC (Toolkit vs Datto vs UI)
# ─────────────────────────────────────────────────────────────────────────
switch ($PSCmdlet.ParameterSetName) {
'Toolkit' {
Write-LogHybrid "Toolkit-only mode" Info Startup
Install-SVSMSP -InstallToolkit
return
}
'Datto' {
Write-LogHybrid "Headless DattoRMM deploy (via n8n)" Info Startup
try{
# ────────────────────────────────────────────
# 1) Fetch URL, Key & Secret from n8n webhook
# ────────────────────────────────────────────
# 1) Fetch creds
$creds = Get-DattoApiCredentials -Password $N8nPassword
if (-not $creds) { throw "Could not fetch creds" }
# 2) Store in globals for everyone else
$Global:ApiUrl = $creds.ApiUrl
$Global:ApiKey = $creds.ApiKey
$Global:ApiSecretKey = $creds.ApiSecretKey
Write-LogHybrid "Fetched Datto API credentials from n8n" Success DattoAuth
}
catch {
Write-LogHybrid "N8N credential fetch error: $($_.Exception.Message)" Error DattoAuth
throw
}
# ────────────────────────────────────────────
# 2) Invoke the existing Install-DattoRMM cmdlet
# ────────────────────────────────────────────
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
}
}
#endregion ScriptMonkey run silently Entrypoint
# 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$tooltip><input type='checkbox' id='$taskId' name='$($_.Name)' data-column='$Column'> $($_.Label)</label>"
if ($_.SubOptions) {
# join inside the code block is fine
$subHtml = (
$_.SubOptions |
ForEach-Object {
"<label style='margin-left:20px; display:block;'>
<input type='checkbox' class='sub-option-$taskId' name='$($_.Value)' value='$($_.Value)'> $($_.Label)
</label>"
}
) -join "`n"
$html += @"
<div id='${taskId}OptionsContainer' style='display:none; margin-top:4px;'>
$subHtml
</div>
"@
}
$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 "<div style='color:#bbb; font-size:0.9em; margin-top:1em;'>Module Version: $($mod.Version)</div>"
}
return "<div style='color:#f66;'>SVSMSP_Module not found</div>"
}
function Get-UIHtml {
param([string]$Page = 'onboard')
#
# 1) Inline your full original CSS here
#
$style = @'
<style>
:root {
/* Cool Palette */
--background-color: rgba(18, 18, 18, 1);
--border-color: rgba(255,127,0,0.25);
/* Neutral Colors */
--white-color: rgba(255,255,255);
--gray-color: rgba(102,102,102);
--dark-gray-color: rgba(51,51,51);
--light-gray-color: rgba(187,187,187);
/* Sidebar Button Colors */
--btn-sidebar-light-gray: rgba(68,68,68);
--btn-sidebar-blue: rgba(30,144,255,1);
--btn-hover: rgba(0,86,179,1);
--btn-hover-scale: 1.05;
/* Button Colors */
--btn-success: rgba(40,167,69);
--btn-success-disabled: rgba(108,117,125);
--btn-danger: rgba(220,53,69);
}
body {
font-family: Arial, sans-serif;
margin: 0; padding: 0;
background-color: var(--background-color);
color: var(--white-color);
height: 100%; overflow: hidden;
}
.logo-container { display: grid; grid-template-columns: auto 1fr; align-items: center; padding: 20px; }
.logo-container img { max-width:300px; height:auto; }
.subtitle { font-size: 1.2rem; color: var(--gray-color); margin-top: 0.5em; }
.container { display:flex; height:100vh; overflow:hidden; }
.sidebar { width:200px; background:var(--background-color); padding:10px; }
.sidebar button {
display:block; width:100%; margin-bottom:10px; padding:10px;
color:var(--white-color); background:var(--btn-sidebar-light-gray);
border:none; border-radius:5px; cursor:pointer; text-align:left;
transition:background-color 0.3s, transform 0.2s;
}
.sidebar button.active { background:var(--btn-sidebar-blue); }
.sidebar button:hover {
background:var(--btn-hover); transform:scale(var(--btn-hover-scale));
}
.content {
position: relative;
flex:1;
padding:20px;
overflow-y:auto;
max-height:calc(100vh - 50px);
}
.fixed-buttons {
position: fixed;
bottom: 20px;
right: 20px;
display: flex;
gap: 10px; /* space between Exit and Run */
z-index: 1000;
}
.exit-button,
.run-button {
border: none;
border-radius: 5px;
padding: 10px 20px;
cursor: pointer;
color: var(--white-color);
}
/* Specific overrides */
.exit-button {
background-color: var(--btn-danger);
}
/* Specific overrides */
.run-button {
background-color: var(--btn-success);
}
.tab-content { display:none; }
.tab-content.active { display:block; }
.columns-container {
display:flex; gap:20px; flex-wrap:wrap; align-items:flex-start;
}
/* column styling, same as old script */
.column {
flex: 1; /* fill available space */
max-width: 45%; /* or whatever width you like */
border: 2px solid var(--border-color);
border-radius: 8px;
padding: 10px;
background-color: var(--dark-gray-color);
box-shadow: 0 4px 6px rgba(0,0,0,0.3);
}
.checkbox-group label {
display:flex; align-items:center; margin-bottom:8px;
}
.button-group { text-align:right; margin-top:20px; }
.exit-button {
background:var(--btn-danger); color:var(--white-color);
padding:10px 20px; border:none; border-radius:5px; cursor:pointer;
}
#PasswordContainer, #dattoRmmContainer {
margin-top: 1em;
}
/* Common styles for inputs, buttons, and selects */
#PasswordContainer input,
#PasswordContainer button,
#dattoRmmContainer select {
background-color: var(--dark-gray-color);
color: var(--white-color);
border: 1px solid var(--border-color);
border-radius: 4px;
padding: 8px;
font-size: 14px;
display: block;
width: 40%;
max-width: 200px;
}
/* Style specifically for the fetch button */
#PasswordContainer button {
background-color: var(--btn-sidebar-blue);
cursor: pointer;
transition: background-color 0.3s ease;
}
/* Hover effect for the fetch button */
#PasswordContainer button:hover {
background-color: var(--btn-hover);
}
/* Tag line */
#tagline {
font-size: 1.2rem;
color: var(--light-gray-color);
font-weight: bold;
justify-self: center;
}
@media (max-width:768px) {
.container { flex-direction:column; }
.sidebar { width:100%; }
}
</style>
'@
$script = @'
<script>
// =======================================================================
// Tab Navigation
// =======================================================================
const tabButtons = document.querySelectorAll(".tab-button");
const tabContents = document.querySelectorAll(".tab-content");
tabButtons.forEach(btn => {
btn.addEventListener("click", () => {
// clear active state
tabButtons.forEach(b => b.classList.remove("active"));
tabContents.forEach(c => c.classList.remove("active"));
// set new active
btn.classList.add("active");
document.getElementById(btn.dataset.tab).classList.add("active");
});
});
// initialize default tab on load
document.querySelector(".tab-button[data-tab='{{defaultPage}}Tab']").classList.add("active");
document.getElementById("{{defaultPage}}Tab").classList.add("active");
// =======================================================================
// Task Trigger
// =======================================================================
const tasks = [
{{tasksJsAll}}
];
// =======================================================================
// Column “Select All” toggling for On-Boarding
// =======================================================================
function toggleColumn(col) {
const master = document.getElementById(`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`);
const children = document.querySelectorAll(`#onboardTab input[type=checkbox][data-column=${col}]`);
children.forEach(cb => {
cb.checked = master.checked;
});
// Now simulate change events after setting all checkboxes
setTimeout(() => {
children.forEach(cb => {
cb.dispatchEvent(new Event('change'));
});
}, 0);
}
// =======================================================================
// Un-check “Select All” if any child is unchecked (& re-check if all are checked)
// =======================================================================
function updateSelectAll(col) {
const master = document.getElementById(
`selectAll${col[0].toUpperCase() + col.slice(1)}Checkbox`
);
const children = document.querySelectorAll(
`#onboardTab input[type=checkbox][data-column=${col}]`
);
master.checked = Array.from(children).every(cb => cb.checked);
}
// Attach listeners on load
['left','right'].forEach(col => {
document
.querySelectorAll(`#onboardTab input[type=checkbox][data-column=${col}]`)
.forEach(cb => cb.addEventListener('change', () => updateSelectAll(col)));
});
// =======================================================================
// DattoRMM Options
// =======================================================================
function toggleDattoRMMOptions() {
const master = document.getElementById('installDattoRMM');
const container = document.getElementById('installDattoRMMOptionsContainer');
if (!container) return;
container.style.display = master.checked ? 'block' : 'none';
container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = master.checked);
}
document.addEventListener('DOMContentLoaded', () => {
const master = document.getElementById('installDattoRMM');
if (master) master.addEventListener('change', toggleDattoRMMOptions);
});
// =======================================================================
// Fetch Sites Handler
// =======================================================================
async function fetchSites() {
const pwd = document.getElementById("Password").value;
if (!pwd) {
alert("Please enter the password.");
return;
}
const dropdown = document.getElementById("dattoDropdown");
dropdown.innerHTML = '<option disabled selected>Loading sites...</option>';
try {
const resp = await fetch("/getpw", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ password: pwd })
});
if (!resp.ok) throw("HTTP " + resp.status);
const sites = await resp.json();
dropdown.innerHTML = ''; // clear the loading message
sites.forEach(site => {
const option = document.createElement("option");
option.value = site.UID;
option.textContent = site.Name;
dropdown.appendChild(option);
});
document.getElementById("dattoRmmContainer").style.display = "block";
}
catch (e) {
console.error(e);
dropdown.innerHTML = '<option disabled selected>Error loading sites</option>';
alert("Failed to fetch sites. Check password and try again.");
}
}
async function triggerInstall() {
// disable the button so you can't double-click while work is in flight
const runBtn = document.querySelector('.run-button');
runBtn.disabled = true;
try {
for (const t of tasks) {
const cb = document.getElementById(t.id);
if (!cb || !cb.checked) continue;
if (t.id === 'installDattoRMM') {
const sub = Array.from(
document.querySelectorAll('.sub-option-installDattoRMM:checked')
).map(x => x.value);
const dropdown = document.getElementById('dattoDropdown');
const uid = dropdown.value;
const name = dropdown.selectedOptions[0].text;
await fetch('/installDattoRMM', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
checkedValues: sub,
UID: uid,
Name: name
})
});
} else {
await fetch(t.handler, { method: 'GET' });
}
}
} catch (e) {
console.error('Error during triggerInstall:', e);
} finally {
// always re-enable, even if an error occurred
runBtn.disabled = false;
}
}
// =======================================================================
// Shutdown Handler
// =======================================================================
function endSession() {
fetch("/quit", { method: "GET" })
.finally(() => window.close());
}
// =======================================================================
// Sub-Options Auto-Toggle for Tasks
// =======================================================================
document.addEventListener('DOMContentLoaded', function () {
// Auto-handle visibility and checking for tasks with sub-options
const tasksWithSubOptions = document.querySelectorAll('[id$="OptionsContainer"]');
tasksWithSubOptions.forEach(container => {
const taskId = container.id.replace('OptionsContainer', '');
const masterCheckbox = document.getElementById(taskId);
if (!masterCheckbox) return;
function updateVisibility() {
const checked = masterCheckbox.checked;
container.style.display = checked ? 'block' : 'none';
container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = checked);
// Show/hide Password and RMM only if it's installDattoRMM
if (taskId === 'installDattoRMM') {
const pwdBox = document.getElementById('PasswordContainer');
const rmmBox = document.getElementById('dattoRmmContainer');
if (pwdBox) pwdBox.style.display = checked ? 'block' : 'none';
if (rmmBox) rmmBox.style.display = checked ? 'block' : 'none';
}
}
masterCheckbox.addEventListener('change', updateVisibility);
updateVisibility(); // call once on load
});
});
// ===========================================
// ─ rotating tagline ───────────────────────────────
// ===========================================
document.addEventListener('DOMContentLoaded', () => {
const taglines = [
"Fast deployments, no monkey business.",
"Bananas for better builds.",
"Deploy without flinging code.",
"Tame your stack. Unleash the monkey.",
"Monkey see, monkey deploy.",
"Deploy smarter -- with a monkey on your team.",
"Don't pass the monkey -- let it deploy.",
"No more monkeying around. Stack handled.",
"Own your stack. But let the monkey do the work.",
"Why throw code when the monkey's got it?",
"Deployments so easy, a monkey could do it. Ours does.",
"Monkey in the stack, not on your back."
];
const el = document.getElementById("tagline");
let idx = Math.floor(Math.random() * taglines.length);
el.textContent = taglines[idx];
setInterval(() => {
idx = (idx + 1) % taglines.length;
el.textContent = taglines[idx];
}, 10_000);
});
// when the browser window is closed (X), notify the server to quit
window.addEventListener('beforeunload', () => {
// keepalive: true ensures the request is sent even as the page unloads
fetch('/quit', { method: 'GET', keepalive: true });
});
</script>
'@
#
# 3) The HTML skeleton with placeholders
#
$htmlTemplate = @"
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Script Monkey</title>
<link rel="icon" href="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_Favicon.ico">
$style
</head>
<body>
<div class="logo-container">
<div class="logo-left">
<img src="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_logo.svg" alt="SVS Logo">
{{moduleVersion}}
</div>
<div id="tagline"></div>
</div>
<div class="container">
<div class="sidebar">
<button class="tab-button" data-tab="onboardTab">On-Boarding</button>
<button class="tab-button" data-tab="offboardTab">Off-Boarding</button>
<button class="tab-button" data-tab="tweaksTab">Tweaks</button>
<button class="tab-button" data-tab="SVSAppsTab">SVS APPs</button>
</div>
<div class="content">
<div id="onboardTab" class="tab-content">
<h2>On-Boarding</h2>
<h3 class="subtitle">This new deployment method ensures everything is successfully deployed with greater ease!</h3>
<!-- 1) Dynamic task checkboxes -->
<div class="columns-container">
<div class="checkbox-group column">
<h3>SVSMSP Stack</h3>
<label><input type="checkbox" id="selectAllLeftCheckbox" onclick="toggleColumn('left')"> Select All</label>
{{onboardLeftColumn}}
</div>
<div class="checkbox-group column">
<h3>Optional</h3>
<label><input type="checkbox" id="selectAllRightCheckbox" onclick="toggleColumn('right')"> Select All</label>
{{onboardRightColumn}}
</div>
</div>
<!-- 2) Password and Datto Site dropdown shown conditionally -->
<div id="PasswordContainer" style="display:none; margin-bottom:1em;">
<label for="Password">Enter Password:</label>
<div style="display:flex; gap:5px;">
<input type="password" id="Password" placeholder="Enter Password" style="flex:1;" />
<button onclick="fetchSites()" style="padding:4px 10px; background-color: var(--btn-sidebar-blue); color: var(--white-color); border: none; border-radius: 4px;">GO!</button>
</div>
</div>
<div id="dattoRmmContainer" style="display:none; margin-bottom:1em;">
<label for="dattoDropdown">Select a Datto RMM site:</label>
<select id="dattoDropdown" style="width:100%;">
<option disabled selected>Fetching sites...</option>
</select>
</div>
<div id="offboardTab" class="tab-content">
<h2>Off-Boarding</h2>
<div class="columns-container">
{{offboardCheckboxes}}
</div>
</div>
<div id="tweaksTab" class="tab-content">
<h2>Tweaks</h2>
<div class="columns-container">
{{tweaksCheckboxes}}
</div>
</div>
<div id="SVSAppsTab" class="tab-content">
<h2>SVS APPs</h2>
<div class="columns-container">
{{appsCheckboxes}}
</div>
</div>
</div>
</div>
$script
<!-- floating button group -->
<div class="fixed-buttons">
<button class="exit-button" onclick="endSession()">Exit</button>
<button class="run-button" onclick="triggerInstall()">Run Selected</button>
</div>
</body>
</html>
"@
#
# 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
# Only launch UI if not in silent mode
if (-not $SilentInstall) {
# open browser on whatever port you've set
# Start-Process "microsoft-edge:http://localhost:$Port"
Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port"
# now start your server (this will block until you hit Exit in the UI)
Start-Server
}
# At the end of ScriptMonkey.ps1
if ($MyInvocation.ExpectingInput) {
Invoke-ScriptMonkey @PSBoundParameters
}