1890 lines
70 KiB
PowerShell
1890 lines
70 KiB
PowerShell
#region changes to be done
|
|
# We could change line 298 and 379 the have it log in "SVS Scripting" instead of "Application" if we can find a way to force create the log
|
|
#endregion changes to be done
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface,
|
|
and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment.
|
|
|
|
.DESCRIPTION
|
|
Install-DattoRMM is a single, unified toolkit for Datto RMM operations. It can be used
|
|
interactively or via HTTP endpoints, and includes built-in validation and error trapping.
|
|
|
|
Key features:
|
|
- Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook.
|
|
- OAuth management - automatically acquires and refreshes bearer tokens over TLS.
|
|
- Site list fetching - returns the list of RMM sites; validates OutputFile to .csv or .json.
|
|
- Site list saving - writes fetched site list to the user's Desktop as CSV or JSON.
|
|
- Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment.
|
|
- Agent download & install - downloads the Datto RMM agent installer and launches it.
|
|
- Installer archiving - saves a copy of the downloaded installer to C:\Temp.
|
|
- HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch
|
|
to log errors and return proper HTTP 500 responses on failure.
|
|
- Idempotent & WhatIf support - uses ShouldProcess for safe, testable agent installs.
|
|
|
|
Throughout, secrets are never written to logs or console, and all operations produce
|
|
clear success/failure messages via Write-LogHybrid.
|
|
|
|
|
|
.PARAMETER UseWebhook
|
|
Switch that forces credential retrieval from the webhook at WebhookUrl using WebhookPassword.
|
|
When omitted, you must supply ApiUrl, ApiKey, and ApiSecretKey directly.
|
|
|
|
.PARAMETER WebhookPassword
|
|
Password to authenticate to the credentials-fetch webhook. Mandatory when -UseWebhook is set.
|
|
|
|
.PARAMETER WebhookUrl
|
|
URL of the credentials webhook endpoint. Defaults to $Global:DattoWebhookUrl.
|
|
|
|
.PARAMETER ApiUrl
|
|
Direct Datto RMM API base URL (used if not fetching from webhook).
|
|
|
|
.PARAMETER ApiKey
|
|
Direct Datto RMM API key (used if not fetching from webhook).
|
|
|
|
.PARAMETER ApiSecretKey
|
|
Direct Datto RMM secret (used if not fetching from webhook).
|
|
|
|
.PARAMETER FetchSites
|
|
Switch to fetch the list of RMM sites and skip all install or variable-push actions.
|
|
|
|
.PARAMETER SaveSitesList
|
|
Switch to save the fetched site list to the desktop as a file named by OutputFile.
|
|
Must be used together with -FetchSites.
|
|
|
|
.PARAMETER OutputFile
|
|
Name of the file to write the site list to (must end in “.csv” or “.json”).
|
|
Defaults to 'datto_sites.csv'.
|
|
|
|
.PARAMETER PushSiteVars
|
|
Switch to fetch site-specific variables and write them under HKLM:\Software\SVS\Deployment.
|
|
|
|
.PARAMETER InstallRMM
|
|
Switch to download and launch the Datto RMM agent installer for the specified site.
|
|
|
|
.PARAMETER SaveCopy
|
|
Switch to save a copy of the downloaded Datto RMM installer into C:\Temp.
|
|
|
|
.PARAMETER SiteUID
|
|
The unique identifier of the Datto RMM site. Mandatory when performing install or variable-push.
|
|
|
|
.PARAMETER SiteName
|
|
The friendly name of the Datto RMM site (used in logging). Mandatory when performing install or variable-push.
|
|
|
|
.EXAMPLE
|
|
|
|
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
|
|
-UseWebhook
|
|
-WebhookPassword 'pwd'
|
|
-SiteUID 'site-123'
|
|
-SiteName 'Acme Corp'
|
|
-PushSiteVars
|
|
-InstallRMM
|
|
|
|
# Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry.
|
|
|
|
.EXAMPLE
|
|
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
|
|
-ApiUrl 'https://api.example.com' `
|
|
-ApiKey 'YourApiKey' `
|
|
-ApiSecretKey 'YourSecretKey' `
|
|
-SiteUID 'site-123' `
|
|
-SiteName 'Acme Corp' `
|
|
-PushSiteVars `
|
|
-InstallRMM
|
|
|
|
# Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry.
|
|
|
|
.EXAMPLE
|
|
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
|
|
-UseWebhook `
|
|
-WebhookPassword 'pwd' `
|
|
-FetchSites `
|
|
-SaveSitesList `
|
|
-OutputFile 'sites.json'
|
|
|
|
# Fetches the full site list via webhook and saves it as JSON to your Desktop.
|
|
|
|
.EXAMPLE
|
|
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
|
|
-ApiUrl 'https://api.example.com' `
|
|
-ApiKey 'YourApiKey' `
|
|
-ApiSecretKey 'YourSecretKey' `
|
|
-SiteUID 'site-123' `
|
|
-SiteName 'Acme Corp' `
|
|
-SaveCopy
|
|
|
|
# Downloads the RMM installer for “Acme Corp” and saves a copy under C:\Temp without running it.
|
|
|
|
.EXAMPLE
|
|
& ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) `
|
|
-ApiUrl 'https://api.example.com' `
|
|
-ApiKey 'YourApiKey' `
|
|
-ApiSecretKey 'YourSecretKey' `
|
|
-SiteUID 'site-123' `
|
|
-SiteName 'Acme Corp' `
|
|
-InstallRMM `
|
|
-WhatIf
|
|
|
|
# Shows what would happen when installing the RMM agent, without making any changes.
|
|
|
|
.EXAMPLE
|
|
& ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall
|
|
|
|
.EXAMPLE
|
|
& ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup
|
|
|
|
#>
|
|
#region Safely bypass Restricted Execution Policy
|
|
# ─── Safely bypass Restricted Execution Policy ───
|
|
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or
|
|
(Get-ExecutionPolicy) -eq 'Restricted') {
|
|
|
|
Write-Host "[Info] Relaunching with ExecutionPolicy Bypass..." -ForegroundColor Yellow
|
|
|
|
if ($PSCommandPath) {
|
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`""
|
|
} else {
|
|
powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }"
|
|
}
|
|
|
|
exit
|
|
}
|
|
|
|
# ─── TLS and silent install defaults ───
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
$ConfirmPreference = 'None'
|
|
#endregion Safely bypass Restricted Execution Policy
|
|
|
|
function Invoke-ScriptMonkey {
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# PARAMETERS + GLOBAL VARIABLES
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
[CmdletBinding(
|
|
DefaultParameterSetName='UI',
|
|
SupportsShouldProcess=$true,
|
|
ConfirmImpact= 'Medium'
|
|
)]
|
|
param(
|
|
# ─────────────────────────────────────────────────────────
|
|
# Toolkit-only mode
|
|
[Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall,
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# remove Toolkit
|
|
[Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup,
|
|
|
|
# ─────────────────────────────────────────────────────────
|
|
# Datto headless mode
|
|
|
|
# ─── DattoFetch & DattoInstall share the webhook creds ─────────────
|
|
[Parameter(Mandatory,ParameterSetName='DattoFetch')]
|
|
[Parameter(Mandatory,ParameterSetName='DattoInstall')]
|
|
[switch]$UseWebhook,
|
|
|
|
[Parameter(Mandatory,ParameterSetName='DattoFetch')]
|
|
[Parameter(Mandatory,ParameterSetName='DattoInstall')]
|
|
[string]$WebhookPassword,
|
|
|
|
[string]$WebhookUrl = $Global:DattoWebhookUrl,
|
|
|
|
# ─── only DattoFetch uses these ────────────────────────────────────
|
|
[Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites,
|
|
[Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList,
|
|
[Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv',
|
|
|
|
# ─── only DattoInstall uses these ─────────────────────────────────
|
|
[Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID,
|
|
[Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName,
|
|
[Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars,
|
|
[Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM,
|
|
[Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy
|
|
)
|
|
|
|
#region global variables
|
|
|
|
# Listening port for HTTP UI
|
|
$Port = 8082
|
|
|
|
# Configurable endpoints
|
|
$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit'
|
|
|
|
# Initialize a global in-memory log cache
|
|
|
|
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
|
|
$Global:LogCache = [System.Collections.ArrayList]::new()
|
|
}
|
|
|
|
#endregion global variables
|
|
|
|
#region SVS Module
|
|
|
|
function Install-SVSMSP {
|
|
param (
|
|
[switch] $Cleanup,
|
|
[switch] $InstallToolkit,
|
|
[Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }),
|
|
[Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }),
|
|
[Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP",
|
|
[Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo",
|
|
[Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/"
|
|
)
|
|
|
|
function Perform-Cleanup {
|
|
Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule"
|
|
|
|
# Attempt to uninstall all versions of SVSMSP
|
|
try {
|
|
Uninstall-Module -Name SVSMSP -AllVersions -Force -ErrorAction Stop
|
|
Write-LogHybrid "SVSMSP module uninstalled from system." "Success" "SVSModule" -LogToEvent
|
|
}
|
|
catch {
|
|
# If no module was found, just warn and continue
|
|
if ($_.Exception.Message -match 'No match was found') {
|
|
Write-LogHybrid "No existing SVSMSP module found to uninstall." "Warning" "SVSModule" -LogToEvent
|
|
}
|
|
else {
|
|
Write-LogHybrid "Failed to uninstall SVSMSP: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
|
|
}
|
|
}
|
|
|
|
# Remove the custom repository if registered
|
|
if (Get-PSRepository -Name SVS_Repo -ErrorAction SilentlyContinue) {
|
|
try {
|
|
Unregister-PSRepository -Name SVS_Repo -ErrorAction Stop
|
|
Write-LogHybrid "SVS_Repo repository unregistered." "Success" "SVSModule" -LogToEvent
|
|
}
|
|
catch {
|
|
Write-LogHybrid "Failed to unregister SVS_Repo: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
|
|
}
|
|
}
|
|
|
|
# Finally, remove it from the current session if loaded
|
|
if (Get-Module -Name SVSMSP) {
|
|
try {
|
|
Remove-Module SVSMSP -Force -ErrorAction Stop
|
|
Write-LogHybrid "SVSMSP module removed from current session." "Success" "SVSModule" -LogToEvent
|
|
}
|
|
catch {
|
|
Write-LogHybrid "Failed to remove SVSMSP from session: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
function Perform-ToolkitInstallation {
|
|
Perform-Cleanup
|
|
Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule" -LogToEvent
|
|
if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
|
|
Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
|
|
}
|
|
Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule" -LogToEvent
|
|
Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force
|
|
Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" -LogToEvent
|
|
}
|
|
|
|
Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent
|
|
if ($Cleanup) {
|
|
Perform-Cleanup; return
|
|
}
|
|
if ($InstallToolkit) {
|
|
Perform-ToolkitInstallation; return
|
|
}
|
|
# default if no switch passed:
|
|
Perform-ToolkitInstallation
|
|
}
|
|
|
|
#endregion SVS Module
|
|
|
|
|
|
|
|
#region Write-Log
|
|
|
|
# This function is used as a fallback if the SVSMSP module is not installed
|
|
# This function is used as a fallback if the SVSMSP module is not installed
|
|
function Write-LogHelper {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory)][string]$Message,
|
|
[ValidateSet("Info","Warning","Error","Success","General")]
|
|
[string]$Level = "Info",
|
|
[string]$TaskCategory = "GeneralTask",
|
|
[switch]$LogToEvent,
|
|
[string]$EventSource = "Script Automation Monkey",
|
|
[string]$EventLog = "Application",
|
|
[int] $CustomEventID,
|
|
[string]$LogFile,
|
|
[switch]$PassThru
|
|
)
|
|
|
|
# ─── IDs & Colors ────────────────────────────────────────────────
|
|
$idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 }
|
|
$colMap = @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" }
|
|
$EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] }
|
|
$color = $colMap[$Level]
|
|
$fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)"
|
|
|
|
# ─── Console Output ─────────────────────────────────────────────
|
|
Write-Host $fmt -ForegroundColor $color
|
|
|
|
# ─── In-Memory Cache ─────────────────────────────────────────────
|
|
|
|
# ─── In-Memory Cache ─────────────────────────────────────────────
|
|
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
|
|
$Global:LogCache = [System.Collections.ArrayList]::new()
|
|
}
|
|
$Global:LogCache.Add([pscustomobject]@{
|
|
Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss')
|
|
Level = $Level
|
|
Message = $fmt
|
|
}) | Out-Null
|
|
|
|
|
|
# ─── File Logging ────────────────────────────────────────────────
|
|
if ($PSBoundParameters.LogFile) {
|
|
try {
|
|
"$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" |
|
|
Out-File -FilePath $LogFile -Append -Encoding UTF8
|
|
}
|
|
catch {
|
|
Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
# ─── Event Log ──────────────────────────────────────────────────
|
|
if ($LogToEvent) {
|
|
try {
|
|
# 1) Ensure your custom source/log exist
|
|
if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
|
|
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
|
|
}
|
|
} catch {
|
|
Write-Host "[Warning] Could not create event log '$EventLog' or source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow
|
|
return
|
|
}
|
|
|
|
# 2) Map level to entry type
|
|
$entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' }
|
|
|
|
# 3) Write to the Windows event log
|
|
try {
|
|
Write-EventLog `
|
|
-LogName $EventLog `
|
|
-Source $EventSource `
|
|
-EntryType $entryType `
|
|
-EventID $EventID `
|
|
-Message $fmt
|
|
}
|
|
catch {
|
|
Write-Host "[Warning] EventLog failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
}
|
|
}
|
|
|
|
if ($PassThru) { return $Global:LogCache[-1] }
|
|
}
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# WRITE-LOG HYBRID (single definition, chooses at runtime if we use the
|
|
# Write-Log from the module or the built-in Write-LogHelper funtions )
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
function Write-LogHybrid {
|
|
[CmdletBinding()]
|
|
param(
|
|
[Parameter(Mandatory=$true)][string]$Message,
|
|
[ValidateSet("Info","Warning","Error","Success","General")]
|
|
[string]$Level = "Info",
|
|
[string]$TaskCategory = "GeneralTask",
|
|
[switch]$LogToEvent,
|
|
[string]$EventSource = "Script Automation Monkey",
|
|
[string]$EventLog = "Application",
|
|
[ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")]
|
|
[string]$ForegroundColorOverride
|
|
)
|
|
|
|
$formatted = "[$Level] [$TaskCategory] $Message"
|
|
|
|
if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) {
|
|
# 1) print to console with the override color
|
|
Write-Host $formatted -ForegroundColor $ForegroundColorOverride
|
|
|
|
# 2) then forward the call (sans the override) to Write-Log or Write-LogHelper
|
|
$invokeParams = @{
|
|
Message = $Message
|
|
Level = $Level
|
|
TaskCategory = $TaskCategory
|
|
LogToEvent = $LogToEvent
|
|
EventSource = $EventSource
|
|
EventLog = $EventLog
|
|
}
|
|
|
|
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
|
Write-Log @invokeParams
|
|
}
|
|
else {
|
|
Write-LogHelper @invokeParams
|
|
}
|
|
}
|
|
else {
|
|
# No override: let Write-Log / Write-LogHelper handle everything (including console color)
|
|
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
|
Write-Log `
|
|
-Message $Message `
|
|
-Level $Level `
|
|
-TaskCategory $TaskCategory `
|
|
-LogToEvent:$LogToEvent `
|
|
-EventSource $EventSource `
|
|
-EventLog $EventLog
|
|
}
|
|
else {
|
|
Write-LogHelper `
|
|
-Message $Message `
|
|
-Level $Level `
|
|
-TaskCategory $TaskCategory `
|
|
-LogToEvent:$LogToEvent `
|
|
-EventSource $EventSource `
|
|
-EventLog $EventLog
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
#endregion Write-Log
|
|
|
|
#region building the Menus
|
|
|
|
# Define every task once here:
|
|
# Id → checkbox HTML `id`
|
|
# Name → URL path (`/Name`)
|
|
# Label → user-visible text
|
|
# HandlerFn → the PowerShell function to invoke
|
|
# Page → which tab/page it appears on
|
|
|
|
$Global:Tasks = @(
|
|
# On-Boarding, left column
|
|
@{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' },
|
|
@{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' },
|
|
@{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' },
|
|
@{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' },
|
|
@{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' },
|
|
@{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' },
|
|
@{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left';
|
|
SubOptions= @(
|
|
@{ Value='inputVar'; Label='Copy Site Variables' },
|
|
@{ Value='rmm'; Label='Install RMM Agent' },
|
|
@{ Value='exe'; Label='Download Executable' }
|
|
)
|
|
},
|
|
|
|
|
|
# On-Boarding, right column (optional bits)
|
|
@{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' },
|
|
@{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' },
|
|
|
|
# Off-Boarding
|
|
@{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' },
|
|
@{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' },
|
|
|
|
# Tweaks
|
|
@{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' },
|
|
|
|
# SVS Apps
|
|
@{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }
|
|
)
|
|
|
|
#endregion building the Menus
|
|
|
|
#region Build-Checkboxes
|
|
function Build-Checkboxes {
|
|
param($Page, $Column)
|
|
|
|
(
|
|
$Global:Tasks |
|
|
Where-Object Page -EQ $Page |
|
|
Where-Object Column -EQ $Column |
|
|
ForEach-Object {
|
|
$taskId = $_.Id
|
|
$tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
|
|
" title='$($_.Tooltip)'"
|
|
} else { '' }
|
|
|
|
$html = "<label$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"
|
|
}
|
|
|
|
#endregion Build-Checkboxes
|
|
|
|
#region Get-ModuleVersionHtml
|
|
|
|
### 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>"
|
|
}
|
|
|
|
#endregion Get-ModuleVersionHtml
|
|
|
|
#region Strat-Server
|
|
# 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-LogHybrid "Listening on http://localhost:$Port/" Info Server
|
|
|
|
|
|
try {
|
|
while ($Global:Listener.IsListening) {
|
|
$ctx = $Global:Listener.GetContext()
|
|
try {
|
|
Dispatch-Request $ctx
|
|
} catch {
|
|
Write-LogHybrid "Dispatch error: $_" "Error" "Server" -LogToEvent
|
|
}
|
|
}
|
|
} finally {
|
|
# once the loop exits, clean up
|
|
$Global:Listener.Close()
|
|
Write-LogHybrid "Listener closed." "Info" "Server" -LogToEvent
|
|
}
|
|
}
|
|
#endregion Strat-Server
|
|
|
|
#region UIHtml
|
|
function Get-UIHtml {
|
|
param([string]$Page = 'onboard')
|
|
|
|
# no spaces before $style
|
|
$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>
|
|
'@
|
|
|
|
# no spaces before $script
|
|
$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 when the "Enter" key is pressed in the password input field
|
|
const passwordField = document.getElementById('Password');
|
|
const goButton = document.querySelector("button[onclick='fetchSites()']");
|
|
|
|
passwordField.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter') { // Check if the key pressed is 'Enter'
|
|
goButton.click(); // Trigger the 'Go' button click
|
|
}
|
|
});
|
|
|
|
// Trigger 'Run Selected' button click when 'Enter' is pressed after selecting a site
|
|
const siteDropdown = document.getElementById('dattoDropdown');
|
|
const runButton = document.querySelector('.run-button');
|
|
|
|
siteDropdown.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' && siteDropdown.value) { // Check if the key pressed is 'Enter' and a site is selected
|
|
runButton.click(); // Trigger the 'Run Selected' button click
|
|
}
|
|
});
|
|
});
|
|
|
|
|
|
// =======================================================================
|
|
// 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() {
|
|
const runBtn = document.querySelector('.run-button');
|
|
if (runBtn) runBtn.disabled = true;
|
|
|
|
const statusBox = document.getElementById('status-box');
|
|
if (statusBox) statusBox.innerHTML = '';
|
|
|
|
try {
|
|
const allTasks = typeof tasks !== 'undefined' ? tasks : [];
|
|
const checkedTasks = allTasks.filter(t => {
|
|
const cb = document.getElementById(t.id);
|
|
return cb && cb.checked;
|
|
});
|
|
|
|
setTotalTaskCount(checkedTasks.length);
|
|
|
|
// Step 1: Run DattoRMM if selected
|
|
const dattoCB = document.getElementById('installDattoRMM');
|
|
if (dattoCB && dattoCB.checked) {
|
|
try {
|
|
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 || 'Datto';
|
|
|
|
await fetch('/installDattoRMM', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ checkedValues: sub, UID: uid, Name: name })
|
|
});
|
|
|
|
logProgress('Install DattoRMM', true);
|
|
} catch (e) {
|
|
logProgress('Install DattoRMM', false);
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
// Step 2: Run SVSMSP module install
|
|
const svsCB = document.getElementById('installSVSMSPModule');
|
|
if (svsCB && svsCB.checked) {
|
|
try {
|
|
await fetch('/installSVSMSPModule', { method: 'GET' });
|
|
logProgress('Install SVSMSP Module', true);
|
|
} catch (e) {
|
|
logProgress('Install SVSMSP Module', false);
|
|
console.error(e);
|
|
}
|
|
}
|
|
|
|
// Step 3: Remaining tasks
|
|
for (const t of checkedTasks) {
|
|
if (['installDattoRMM', 'installSVSMSPModule'].includes(t.id)) continue;
|
|
|
|
try {
|
|
await fetch(t.handler, { method: 'GET' });
|
|
logProgress(t.label || t.id, true);
|
|
} catch (e) {
|
|
logProgress(t.label || t.id, false);
|
|
console.error(`Error running ${t.id}:`, e);
|
|
}
|
|
}
|
|
} catch (mainErr) {
|
|
console.error('triggerInstall failed:', mainErr);
|
|
} finally {
|
|
if (runBtn) 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>
|
|
let completedTasks = 0;
|
|
let totalTasks = 0;
|
|
|
|
function setTotalTaskCount(count) {
|
|
totalTasks = count;
|
|
completedTasks = 0;
|
|
updateTitle();
|
|
}
|
|
|
|
function logProgress(label, isSuccess) {
|
|
const statusBox = document.getElementById('status-box');
|
|
completedTasks++;
|
|
updateTitle();
|
|
|
|
const msg = isSuccess
|
|
? `✅ ${completedTasks}/${totalTasks} done: ${label}`
|
|
: `❌ ${completedTasks}/${totalTasks} failed: ${label}`;
|
|
|
|
const div = document.createElement('div');
|
|
div.style.color = isSuccess ? 'lime' : 'red';
|
|
div.textContent = msg;
|
|
statusBox.appendChild(div);
|
|
|
|
if (completedTasks === totalTasks) {
|
|
const finalMsg = document.createElement('div');
|
|
finalMsg.style.marginTop = '10px';
|
|
finalMsg.innerHTML = `<strong>✅ All tasks complete (${completedTasks}/${totalTasks})</strong>`;
|
|
statusBox.appendChild(finalMsg);
|
|
|
|
document.title = `✅ ScriptMonkey - Complete (${completedTasks}/${totalTasks})`;
|
|
|
|
// Optional: Play sound
|
|
const sound = new Audio('data:audio/wav;base64,UklGRiQAAABXQVZFZm10IBAAAAABAAEAESsAACJWAAACABAAZGF0YQAAAAA=');
|
|
sound.play().catch(() => {});
|
|
|
|
// Optional: Flash title
|
|
flashTitle(`✅ ScriptMonkey - Complete (${completedTasks}/${totalTasks})`);
|
|
}
|
|
}
|
|
|
|
function updateTitle() {
|
|
document.title = `ScriptMonkey - ${completedTasks}/${totalTasks} Done`;
|
|
}
|
|
|
|
function flashTitle(finalTitle) {
|
|
let flashes = 0;
|
|
const interval = setInterval(() => {
|
|
document.title = (document.title === '') ? finalTitle : '';
|
|
flashes++;
|
|
if (flashes >= 10) {
|
|
clearInterval(interval);
|
|
document.title = finalTitle;
|
|
}
|
|
}, 800);
|
|
}
|
|
</script>
|
|
|
|
|
|
|
|
</script>
|
|
|
|
'@
|
|
|
|
# no spaces before $htmlTemplate
|
|
$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">
|
|
<!-- SVS Logo (left) -->
|
|
<div class="logo-left">
|
|
<img src="https://git.svstools.com/syelle/Logo/raw/branch/main/SVS_logo.svg" alt="SVS Logo">
|
|
{{moduleVersion}}
|
|
</div>
|
|
|
|
<!-- SAMY Logo (right) -->
|
|
<div class="logo-right" style="text-align:right;">
|
|
<img src="https://git.svstools.com/syelle/Logo/raw/branch/main/SAMY.png" alt="SAMY Logo" style="max-width:180px;">
|
|
<div id="tagline" style="font-size:1.1rem; color:var(--light-gray-color); font-weight:bold;">Script Automation Monkey (Yeah!)</div>
|
|
</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 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()
|
|
}
|
|
|
|
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)
|
|
|
|
try {
|
|
# 1) Read the incoming JSON payload (contains only the webhook password)
|
|
$raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
|
|
$pw = (ConvertFrom-Json $raw).password
|
|
|
|
# ★ Store it globally for the next call ★
|
|
$Global:WebhookPassword = $pw
|
|
|
|
# 2) Delegate to your unified function
|
|
$sites = Install-DattoRMM `
|
|
-UseWebhook `
|
|
-WebhookPassword $pw `
|
|
-FetchSites `
|
|
-SaveSitesList:$SaveSitesList `
|
|
-OutputFile $OutputFile
|
|
|
|
# 3) Return JSON array of sites
|
|
Respond-JSON $Context $sites
|
|
}
|
|
catch {
|
|
# Log the exception and return HTTP 500
|
|
Write-LogHybrid "Handle-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
$Context.Response.StatusCode = 500
|
|
Respond-Text $Context "Internal server error fetching sites."
|
|
}
|
|
}
|
|
|
|
# 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)
|
|
|
|
try {
|
|
if ($Context.Request.HttpMethod -ne 'POST') {
|
|
$Context.Response.StatusCode = 405
|
|
Respond-Text $Context 'Use POST'
|
|
return
|
|
}
|
|
|
|
# 1) Read and parse the JSON body
|
|
$body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
|
|
$data = ConvertFrom-Json $body
|
|
|
|
# 2) Delegate to your unified function for the install
|
|
Install-DattoRMM `
|
|
-UseWebhook `
|
|
-WebhookPassword $Global:WebhookPassword `
|
|
-SiteUID $data.UID `
|
|
-SiteName $data.Name `
|
|
-PushSiteVars:($data.checkedValues -contains 'inputVar') `
|
|
-InstallRMM: ($data.checkedValues -contains 'rmm') `
|
|
-SaveCopy: ($data.checkedValues -contains 'exe')
|
|
|
|
# 3) Acknowledge to the client
|
|
Respond-Text $Context "Triggered DattoRMM for $($data.Name)"
|
|
}
|
|
catch {
|
|
# Log the exception and return HTTP 500
|
|
Write-LogHybrid "Handle-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
$Context.Response.StatusCode = 500
|
|
Respond-Text $Context "Internal server error during DattoRMM install."
|
|
}
|
|
}
|
|
#endregion Handler Stubs
|
|
|
|
#region Install-DattoRMM
|
|
|
|
<#
|
|
.SYNOPSIS
|
|
Installs/configures the Datto RMM agent, fetches site lists, and optionally saves the site list to disk.
|
|
|
|
.DESCRIPTION
|
|
Centralizes Datto RMM operations in one function:
|
|
- Fetch API credentials from a webhook (-UseWebhook)
|
|
- Acquire OAuth token
|
|
- Fetch site list (-FetchSites)
|
|
- Save site list to Desktop as JSON or CSV (-FetchSites + -SaveSitesList)
|
|
- Write site variables to registry (-PushSiteVars)
|
|
- Download & launch the RMM agent installer (-InstallRMM)
|
|
- Save a copy of the installer (-SaveCopy)
|
|
|
|
.PARAMETER UseWebhook
|
|
Fetches ApiUrl, ApiKey, and ApiSecretKey from the webhook when used with WebhookPassword.
|
|
|
|
.PARAMETER WebhookPassword
|
|
Password for authenticating to the credentials webhook.
|
|
|
|
.PARAMETER WebhookUrl
|
|
URL of the credentials webhook. Defaults to $Global:DattoWebhookUrl.
|
|
|
|
.PARAMETER ApiUrl
|
|
Direct Datto API endpoint URL (if not using webhook).
|
|
|
|
.PARAMETER ApiKey
|
|
Direct Datto API key (if not using webhook).
|
|
|
|
.PARAMETER ApiSecretKey
|
|
Direct Datto API secret (if not using webhook).
|
|
|
|
.PARAMETER FetchSites
|
|
Fetches the list of sites and skips all install steps.
|
|
|
|
.PARAMETER SaveSitesList
|
|
Saves the fetched site list to Desktop using OutputFile. Must be used with -FetchSites.
|
|
|
|
.PARAMETER OutputFile
|
|
Filename for saving the site list (.json or .csv). Defaults to 'datto_sites.csv'.
|
|
|
|
.PARAMETER PushSiteVars
|
|
Writes fetched site variables into HKLM:\Software\SVS\Deployment.
|
|
|
|
.PARAMETER InstallRMM
|
|
Downloads and runs the Datto RMM agent installer.
|
|
|
|
.PARAMETER SaveCopy
|
|
Saves a copy of the downloaded agent installer to C:\Temp.
|
|
|
|
.PARAMETER SiteUID
|
|
Unique identifier of the Datto site (required for install and registry push).
|
|
|
|
.PARAMETER SiteName
|
|
Friendly name of the Datto site (used for logging).
|
|
|
|
.EXAMPLE
|
|
# Fetch and save site list via webhook
|
|
Install-DattoRMM -UseWebhook -WebhookPassword 'Tndmeeisdwge!' -FetchSites -SaveSitesList -OutputFile 'sites.csv'
|
|
|
|
.EXAMPLE
|
|
# Headless install with site variables
|
|
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
|
|
-SiteUID 'site-123' -SiteName 'Acme Corp' -PushSiteVars -InstallRMM
|
|
|
|
.EXAMPLE
|
|
# Download and save installer to C:\Temp without installing
|
|
Install-DattoRMM -ApiUrl 'https://api.example.com' -ApiKey 'KeyHere' -ApiSecretKey 'SecretHere' \
|
|
-SiteUID 'site-123' -SiteName 'Acme Corp' -SaveCopy
|
|
#>
|
|
function Install-DattoRMM {
|
|
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
|
|
param (
|
|
[switch]$UseWebhook,
|
|
[string]$WebhookPassword,
|
|
[string]$WebhookUrl = $Global:DattoWebhookUrl,
|
|
[string]$ApiUrl,
|
|
[string]$ApiKey,
|
|
[string]$ApiSecretKey,
|
|
[switch]$FetchSites,
|
|
[switch]$SaveSitesList,
|
|
[string]$OutputFile = 'datto_sites.csv',
|
|
[switch]$PushSiteVars,
|
|
[switch]$InstallRMM,
|
|
[switch]$SaveCopy,
|
|
[string]$SiteUID,
|
|
[string]$SiteName
|
|
)
|
|
|
|
# Validate mutually-dependent switches
|
|
if ($SaveSitesList -and -not $FetchSites) {
|
|
Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM -LogToEvent; return
|
|
}
|
|
|
|
# 1) Optionally fetch credentials from webhook
|
|
if ($UseWebhook) {
|
|
if (-not $WebhookPassword) {
|
|
Write-LogHybrid "Webhook password missing." Error DattoRMM -LogToEvent; return
|
|
}
|
|
try {
|
|
$resp = Invoke-RestMethod -Uri $WebhookUrl `
|
|
-Headers @{ SVSMSPKit = $WebhookPassword } `
|
|
-Method GET
|
|
$ApiUrl = $resp.ApiUrl
|
|
$ApiKey = $resp.ApiKey
|
|
$ApiSecretKey = $resp.ApiSecretKey
|
|
Write-LogHybrid "Webhook credentials fetched." Success DattoRMM -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM -LogToEvent; return
|
|
}
|
|
}
|
|
|
|
# 2) Validate API parameters
|
|
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
|
|
Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent; return
|
|
}
|
|
|
|
# 3) Acquire OAuth token
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
try {
|
|
$publicCred = New-Object System.Management.Automation.PSCredential(
|
|
'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force)
|
|
)
|
|
$tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" `
|
|
-Credential $publicCred `
|
|
-Method Post `
|
|
-ContentType 'application/x-www-form-urlencoded' `
|
|
-Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey"
|
|
$token = $tokenResp.access_token
|
|
Write-LogHybrid "OAuth token acquired." Success DattoRMM -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM-LogToEvent; return
|
|
}
|
|
$headers = @{ Authorization = "Bearer $token" }
|
|
|
|
# 4) Fetch site list only
|
|
if ($FetchSites) {
|
|
try {
|
|
$sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers
|
|
$siteList = $sitesResp.sites | ForEach-Object {
|
|
[PSCustomObject]@{ Name = $_.name; UID = $_.uid }
|
|
}
|
|
Write-LogHybrid "Fetched $($siteList.Count) sites." Success DattoRMM -LogToEvent
|
|
|
|
if ($SaveSitesList) {
|
|
$desktop = [Environment]::GetFolderPath('Desktop')
|
|
$path = Join-Path $desktop $OutputFile
|
|
$ext = [IO.Path]::GetExtension($OutputFile).ToLower()
|
|
if ($ext -eq '.json') {
|
|
$siteList | ConvertTo-Json -Depth 3 | Out-File -FilePath $path -Encoding UTF8
|
|
} else {
|
|
$siteList | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
|
|
}
|
|
Write-LogHybrid "Wrote $($siteList.Count) sites to $path" Success DattoRMM -LogToEvent
|
|
}
|
|
|
|
return $siteList
|
|
} catch {
|
|
Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM -LogToEvent; return @()
|
|
}
|
|
}
|
|
|
|
# 5) Push site variables to registry
|
|
if ($PushSiteVars) {
|
|
try {
|
|
$varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers
|
|
Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
}
|
|
$regPath = "HKLM:\Software\SVS\Deployment"
|
|
foreach ($v in $varsResp.variables) {
|
|
try {
|
|
if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
|
|
New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null
|
|
Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
}
|
|
}
|
|
}
|
|
|
|
# 6) Download & install RMM agent
|
|
if ($InstallRMM) {
|
|
if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) {
|
|
try {
|
|
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
|
|
$tmp = "$env:TEMP\AgentInstall.exe"
|
|
Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing
|
|
Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM -LogToEvent
|
|
Start-Process -FilePath $tmp -NoNewWindow
|
|
Write-LogHybrid "RMM agent installer launched." Success DattoRMM -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
}
|
|
}
|
|
}
|
|
|
|
# 7) Save a copy of installer to C:\Temp
|
|
if ($SaveCopy) {
|
|
try {
|
|
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
|
|
$path = "C:\Temp\AgentInstall.exe"
|
|
if (-not (Test-Path 'C:\Temp')) { New-Item -Path 'C:\Temp' -ItemType Directory | Out-Null }
|
|
Invoke-WebRequest -Uri $dlUrl -OutFile $path -UseBasicParsing
|
|
Write-LogHybrid "Saved installer copy to $path." Info DattoRMM -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Save-copy failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
}
|
|
}
|
|
|
|
# 8) Warn if no action was taken
|
|
if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
|
|
Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning DattoRMM -LogToEvent
|
|
}
|
|
}
|
|
|
|
|
|
#endregion Install-DattoRMM
|
|
|
|
#region Dispatch-Request
|
|
|
|
# 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" -LogToEvent
|
|
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 Dispatch-Request
|
|
|
|
#region EntryPoint: Define Invoke-ScriptMonkey
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
# 3) MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI)
|
|
# ─────────────────────────────────────────────────────────────────────────
|
|
|
|
switch ($PSCmdlet.ParameterSetName) {
|
|
'Toolkit' {
|
|
Write-LogHybrid "Toolkit-only mode" Info Startup -LogToEvent
|
|
Install-SVSMSP -InstallToolkit
|
|
return
|
|
}
|
|
|
|
'Cleanup' {
|
|
Write-LogHybrid "Running Toolkit cleanup mode" Info Startup -LogToEvent
|
|
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 -LogToEvent
|
|
$sites = Install-DattoRMM `
|
|
-UseWebhook `
|
|
-WebhookPassword $WebhookPassword `
|
|
-FetchSites `
|
|
-SaveSitesList:$SaveSitesList `
|
|
-OutputFile $OutputFile
|
|
|
|
Write-LogHybrid "Done." Success DattoAuth -LogToEvent
|
|
return
|
|
}
|
|
|
|
|
|
# ────────────────────────────────────────────
|
|
# 3) Invoke the existing Install-DattoRMM cmdlet
|
|
# ────────────────────────────────────────────
|
|
|
|
'DattoInstall' {
|
|
Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth -LogToEvent
|
|
|
|
if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) {
|
|
Install-DattoRMM `
|
|
-UseWebhook `
|
|
-WebhookPassword $WebhookPassword `
|
|
-SiteUID $SiteUID `
|
|
-SiteName $SiteName `
|
|
-PushSiteVars:$PushSiteVars `
|
|
-InstallRMM:$InstallRMM `
|
|
-SaveCopy:$SaveCopy
|
|
}
|
|
|
|
return
|
|
|
|
}
|
|
|
|
'UI' {
|
|
Write-LogHybrid "Starting ScriptMonkey UI on http://localhost:$Port/" Info Startup
|
|
Start-Process "msedge.exe" -ArgumentList "--app=http://localhost:$Port"
|
|
Start-Server # blocks until you click Exit
|
|
return
|
|
}
|
|
|
|
}
|
|
#endregion EntryPoint: Define Invoke-ScriptMonkey
|
|
|
|
|
|
#region — guarantee NuGet provider is present without prompting
|
|
|
|
# ─── Silent defaults ───
|
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
$ProgressPreference = 'SilentlyContinue'
|
|
$ConfirmPreference = 'None'
|
|
|
|
# ─── Pre-create folder if running as SYSTEM (avoids NuGet install bug) ───
|
|
$provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies"
|
|
if (-not (Test-Path $provPath)) {
|
|
try {
|
|
New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warn Bootstrap -LogToEvent
|
|
}
|
|
}
|
|
|
|
# ─── Ensure PowerShellGet is available ───
|
|
if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) {
|
|
try {
|
|
Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop
|
|
Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
|
|
}
|
|
}
|
|
|
|
# ─── Ensure PackageManagement is up-to-date ───
|
|
$pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version
|
|
if ($pkgMgmtVersion -lt [Version]"1.3.1") {
|
|
try {
|
|
Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop
|
|
Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warn Bootstrap -LogToEvent
|
|
}
|
|
}
|
|
|
|
# ─── Import modules silently ───
|
|
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
|
|
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
|
|
|
|
# ─── Trust PSGallery if not already ───
|
|
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
|
|
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
|
|
try {
|
|
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
|
|
Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warn Bootstrap -LogToEvent
|
|
}
|
|
}
|
|
|
|
# ─── Ensure NuGet is installed silently ───
|
|
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
|
|
if (-not $nuget) {
|
|
try {
|
|
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop
|
|
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
|
|
Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent
|
|
} catch {
|
|
Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
|
|
}
|
|
} else {
|
|
Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent
|
|
}
|
|
|
|
# ─── Final import check ───
|
|
try {
|
|
Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null
|
|
} catch {
|
|
Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
|
|
}
|
|
|
|
#endregion guarantee NuGet provider is present without prompting
|
|
|
|
|
|
#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
|
|
}
|
|
}
|
|
|
|
|