diff --git a/samy.ps1 b/samy.ps1
index c28603f..e98097e 100644
--- a/samy.ps1
+++ b/samy.ps1
@@ -2,7 +2,7 @@
#endregion changes to be done
-## Last changes made should fix the issues we had when running this in Windows 11 25H2
+## Last changes made should fix the issues we had wen running thi in Windows 11 25H2
<#
.SYNOPSIS
@@ -19,24 +19,72 @@
- 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 and install - downloads the Datto RMM agent installer and launches it.
+ - 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 and WhatIf support - uses ShouldProcess for safe, testable agent installs.
+ - 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 `
+ -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 )) `
@@ -48,6 +96,8 @@
-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 `
@@ -56,6 +106,8 @@
-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' `
@@ -65,6 +117,8 @@
-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' `
@@ -75,6 +129,8 @@
-InstallRMM `
-WhatIf
+ # Shows what would happen when installing the RMM agent, without making any changes.
+
.EXAMPLE
& ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall
@@ -82,9 +138,8 @@
& ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup
#>
-
#region Safely bypass Restricted Execution Policy
-# --- Safely bypass Restricted Execution Policy ---
+# ─── Safely bypass Restricted Execution Policy ───
if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or
(Get-ExecutionPolicy) -eq 'Restricted') {
@@ -99,254 +154,281 @@ if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or
exit
}
-# --- TLS and silent install defaults ---
+# ─── 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 + GLOBALS
- # ---------------------------------------------------------------------
+ function Invoke-ScriptMonkey {
+
+ # ─────────────────────────────────────────────────────────────────────────
+ # PARAMETERS + GLOBAL VARIABLES
+ # ─────────────────────────────────────────────────────────────────────────
+
[CmdletBinding(
- DefaultParameterSetName = 'UI',
- SupportsShouldProcess = $true,
- ConfirmImpact = 'Medium'
+ DefaultParameterSetName='UI',
+ SupportsShouldProcess=$true,
+ ConfirmImpact= 'Medium'
)]
param(
+ # ─────────────────────────────────────────────────────────
# Toolkit-only mode
- [Parameter(Mandatory, ParameterSetName = 'Toolkit')]
- [switch]$SilentInstall,
+ [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall,
+
+ # ─────────────────────────────────────────────────────────
+ # remove Toolkit
+ [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup,
- # Remove toolkit
- [Parameter(Mandatory, ParameterSetName = 'Cleanup')]
- [switch]$Cleanup,
+ # ─────────────────────────────────────────────────────────
+ # Datto headless mode
- # Datto headless mode: shared webhook bits
- [Parameter(Mandatory, ParameterSetName = 'DattoFetch')]
- [Parameter(Mandatory, ParameterSetName = 'DattoInstall')]
+ # ─── DattoFetch & DattoInstall share the webhook creds ─────────────
+ [Parameter(Mandatory,ParameterSetName='DattoFetch')]
+ [Parameter(Mandatory,ParameterSetName='DattoInstall')]
[switch]$UseWebhook,
- [Parameter(Mandatory, ParameterSetName = 'DattoFetch')]
- [Parameter(Mandatory, ParameterSetName = 'DattoInstall')]
+ [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',
- # DattoFetch only
- [Parameter(ParameterSetName = 'DattoFetch')]
- [switch]$FetchSites,
-
- [Parameter(ParameterSetName = 'DattoFetch')]
- [switch]$SaveSitesList,
-
- [Parameter(ParameterSetName = 'DattoFetch')]
- [ValidatePattern('\.csv$|\.json$')]
- [string]$OutputFile = 'datto_sites.csv',
-
- # DattoInstall only
- [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
+ # ─── 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
)
-
- # ---------------------------------------------------------------------
- # GLOBAL VARIABLES
- # ---------------------------------------------------------------------
+
+ #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()
+ $Global:LogCache = [System.Collections.ArrayList]::new()
}
- # =====================================================================
- # SVS MODULE
- # =====================================================================
- function Initialize-NuGetProvider {
- [CmdletBinding()]
- param()
+ #endregion global variables
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- $ProgressPreference = 'SilentlyContinue'
- $ConfirmPreference = 'None'
+ #region SVS Module
- $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)" Warning Bootstrap -LogToEvent
+ function Initialize-NuGetProvider {
+ [CmdletBinding()]
+ param()
+
+ #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)" Warning Bootstrap -LogToEvent
+ }
}
- }
- 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 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
+ }
}
- }
- $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)" Warning 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)" Warning Bootstrap -LogToEvent
+ }
}
- }
- Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
- Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
+ # ─── Import modules silently ───
+ Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
+ Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
- $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)" Warning Bootstrap -LogToEvent
+ # ─── 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)" Warning Bootstrap -LogToEvent
+ }
}
- }
- $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
+ # ─── 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
}
- } 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
}
- try {
- Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null
- } catch {
- Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
- }
- }
- function Install-SVSMSP {
+ 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/"
- )
+ [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
+ 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
+ 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
+ Write-LogHybrid "No existing SVSMSP module found to uninstall." "Warning" "SVSModule" -LogToEvent
}
else {
- Write-LogHybrid "Failed to uninstall SVSMSP: $($_.Exception.Message)" Error SVSModule -LogToEvent
+ 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
+ Write-LogHybrid "SVS_Repo repository unregistered." "Success" "SVSModule" -LogToEvent
}
catch {
- Write-LogHybrid "Failed to unregister SVS_Repo: $($_.Exception.Message)" Error SVSModule -LogToEvent
+ 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
+ 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
+ Write-LogHybrid "Failed to remove SVSMSP from session: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
}
}
}
- function Remove-SVSDeploymentRegKey {
- $regKey = 'HKLM:\Software\SVS'
+ function Remove-SVSDeploymentRegKey {
+ $regKey = 'HKLM:\Software\SVS'
+
+ try {
+ if (Test-Path $regKey) {
+ Remove-Item -Path $regKey -Recurse -Force
+ Write-LogHybrid "Registry key '$regKey' deleted successfully." "Success" "SVSModule" -LogToEvent
+ }
+ else {
+ Write-LogHybrid "Registry key '$regKey' not found; nothing to delete." "Info" "SVSModule" -LogToEvent
+ }
+ }
+ catch {
+ Write-LogHybrid "Failed to delete registry key '$regKey': $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent
+ }
+ }
+
- try {
- if (Test-Path $regKey) {
- Remove-Item -Path $regKey -Recurse -Force
- Write-LogHybrid "Registry key '$regKey' deleted successfully." Success SVSModule -LogToEvent
- }
- else {
- Write-LogHybrid "Registry key '$regKey' not found; nothing to delete." Info SVSModule -LogToEvent
- }
- }
- catch {
- Write-LogHybrid "Failed to delete registry key '$regKey': $($_.Exception.Message)" Error SVSModule -LogToEvent
- }
- }
function Perform-ToolkitInstallation {
- Initialize-NuGetProvider
- Perform-Cleanup
+ Initialize-NuGetProvider
+ 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 "Registering repo $NewRepositoryName..." Info SVSModule -LogToEvent
- if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) {
- Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted
- }
+ Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent
+ if ($Cleanup) {
- 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
Remove-SVSDeploymentRegKey
return
+
+ }
+ if ($InstallToolkit) {
+ Perform-ToolkitInstallation; return
+ }
+ # default if no switch passed:
+ Perform-ToolkitInstallation
}
- if ($InstallToolkit) {
- Perform-ToolkitInstallation
- return
- }
+ #endregion SVS Module
- Perform-ToolkitInstallation
- }
+
- # =====================================================================
- # WRITE-LOG (primary implementation)
- # =====================================================================
+ #region Write-Log
+
+ # Fallback logger used when the SVSMSP module (and its Write-Log) is not available.
+ # Mirrors the behaviour of the toolkit Write-Log (v1.5), including:
+ # - Default EventLog: "SVSMSP Events" (out of Application log)
+ # - Default EventSource: "SVSMSP_Module"
+ # - Level-based Event IDs and console colors
+ # - Global in-memory log cache
+ # - One-time Event Log/source initialization with optional auto-elevation
function Write-LogHelper {
<#
.SYNOPSIS
- Standardized logging utility with console/file output and Windows Event Log support.
+ Standardized logging utility with console/file output and Windows Event Log support,
+ including one-time event source initialization and optional auto-elevated creation
+ of a custom log/source. (Fallback implementation for ScriptMonkey.)
+
+ .DESCRIPTION
+ Mirrors the SVSMSP toolkit Write-Log so that Write-LogHybrid can safely fall back
+ when the module isn't loaded.
.NOTES
Default EventLog : SVSMSP Events
@@ -366,6 +448,7 @@ function Invoke-ScriptMonkey {
[string]$EventSource = "SVSMSP_Module",
+ # Custom log name so you get your own node under "Applications and Services Logs"
[string]$EventLog = "SVSMSP Events",
[int]$CustomEventID,
@@ -375,6 +458,7 @@ function Invoke-ScriptMonkey {
[switch]$PassThru
)
+ # ---------- Event ID / console color ----------
$EventID = if ($CustomEventID) { $CustomEventID } else {
switch ($Level) {
"Info" { 1000 }
@@ -396,6 +480,7 @@ function Invoke-ScriptMonkey {
$FormattedMessage = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)"
Write-Host $FormattedMessage -ForegroundColor $Color
+ # ---------- In-memory cache ----------
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
$Global:LogCache = [System.Collections.ArrayList]::new()
}
@@ -407,6 +492,7 @@ function Invoke-ScriptMonkey {
}
[void]$Global:LogCache.Add($logEntry)
+ # ---------- Optional file output ----------
if ($LogFile) {
try {
"$($logEntry.Timestamp) $FormattedMessage" |
@@ -417,7 +503,10 @@ function Invoke-ScriptMonkey {
}
}
+ # ---------- Windows Event Log handling with one-time init + optional auto-elevate ----------
if ($LogToEvent) {
+
+ # Per-run cache for (LogName|Source) init state
if (-not $Global:EventSourceInitState) {
$Global:EventSourceInitState = @{}
}
@@ -426,7 +515,7 @@ function Invoke-ScriptMonkey {
"Info" { "Information" }
"Warning" { "Warning" }
"Error" { "Error" }
- "Success" { "Information" }
+ "Success" { "Information" } # treat success as info in Event Log
default { "Information" }
}
@@ -436,7 +525,10 @@ function Invoke-ScriptMonkey {
-not $Global:EventSourceInitState[$sourceKey]) {
try {
+ # Only bother if the source doesn't already exist
if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
+
+ # Check if current token is admin
$isAdmin = $false
try {
$current = [Security.Principal.WindowsIdentity]::GetCurrent()
@@ -450,9 +542,11 @@ function Invoke-ScriptMonkey {
}
if ($isAdmin) {
+ # Elevated already: create log/source directly
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
}
else {
+ # Not elevated: run a one-off helper as admin to create log/source
$helperScript = @"
if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
New-EventLog -LogName '$EventLog' -Source '$EventSource'
@@ -467,6 +561,7 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
$helperScript | Set-Content -Path $tempPath -Encoding UTF8
try {
+ # This will trigger UAC prompt in interactive sessions
$null = Start-Process -FilePath "powershell.exe" `
-ArgumentList "-ExecutionPolicy Bypass -File `"$tempPath`"" `
-Verb RunAs -Wait -PassThru
@@ -480,6 +575,7 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
}
}
+ # Re-check after creation attempt
if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
$Global:EventSourceInitState[$sourceKey] = $true
}
@@ -494,6 +590,7 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
}
}
+ # Only write if initialization succeeded
if ($Global:EventSourceInitState[$sourceKey]) {
try {
$EventMessage = "TaskCategory: $TaskCategory | Message: $Message"
@@ -504,12 +601,20 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
}
}
}
+ # ------------------------------------------------------------------------------------------
if ($PassThru) {
return $logEntry
}
}
-
+
+ # ─────────────────────────────────────────────────────────────────────────
+ # WRITE-LOG HYBRID
+ # Uses module Write-Log if present; otherwise falls back to Write-LogHelper.
+ # Defaults aligned with toolkit:
+ # EventSource = "SVSMSP_Module"
+ # EventLog = "SVSMSP Events"
+ # ─────────────────────────────────────────────────────────────────────────
function Write-LogHybrid {
[CmdletBinding()]
param(
@@ -539,6 +644,7 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
$formatted = "[$Level] [$TaskCategory] $Message"
+ # Build the common parameter set for forwarding into Write-Log / Write-LogHelper
$invokeParams = @{
Message = $Message
Level = $Level
@@ -559,8 +665,10 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
}
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
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
Write-Log @invokeParams
}
@@ -569,6 +677,7 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
}
}
else {
+ # No override: let Write-Log / Write-LogHelper handle everything (including console color)
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
Write-Log @invokeParams
}
@@ -578,222 +687,381 @@ if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
}
}
- # =====================================================================
- # SAMY TASK DEFINITIONS
- # =====================================================================
- $Global:SamyTasks = @(
+ #endregion Write-Log
+
+
+ # This function is used as a fallback if the SVSMSP module is not installed
+ # Should change this "[string]$EventLog = "Application", => [string]$EventLog = "SVS Scripting", "
+ 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 )
+ # Should chanfge this "[string]$EventLog = "Application"," => "[string]$EventLog = "SVS Scripting","
+ # ─────────────────────────────────────────────────────────────────────────
+
+ 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:SamyTasks = @(
# On-Boarding, left column
- @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-SetSVSPowerPlan'; Page='onboard'; Column='left' },
+ @{ 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='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' }
- )
+ 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' },
+ @{ 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='offUninstallCyberQP'; Name='offUninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Handle-UninstallCyberQP'; Page='offboard' },
+ @{ Id='offUninstallSVSHelpDesk'; Name='offUninstallSVSHelpDesk'; Label='Uninstall SVS HelpDesk'; HandlerFn='Handle-UninstallSVSHelpDesk'; Page='offboard' },
+ @{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker'; HandlerFn='Handle-UninstallThreatLocker'; Page='offboard' },
+ @{ Id='offUninstallRocketCyber'; Name='offUninstallRocketCyber'; Label='Uninstall RocketCyber'; HandlerFn='Handle-UninstallRocketCyber'; Page='offboard' },
+ @{ Id='offCleanupSVSMSPModule'; Name='offCleanupSVSMSPModule'; Label='Cleanup SVSMSP Toolkit'; HandlerFn='Handle-CleanupSVSMSP'; Page='offboard' },
- # Off-Boarding
- @{ Id='offUninstallCyberQP'; Name='offUninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Handle-UninstallCyberQP'; Page='offboard' },
- @{ Id='offUninstallSVSHelpDesk'; Name='offUninstallSVSHelpDesk'; Label='Uninstall SVS HelpDesk'; HandlerFn='Handle-UninstallSVSHelpDesk'; Page='offboard' },
- @{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker'; HandlerFn='Handle-UninstallThreatLocker'; Page='offboard' },
- @{ Id='offUninstallRocketCyber'; Name='offUninstallRocketCyber'; Label='Uninstall RocketCyber'; HandlerFn='Handle-UninstallRocketCyber'; Page='offboard' },
- @{ Id='offCleanupSVSMSPModule'; Name='offCleanupSVSMSPModule'; Label='Cleanup SVSMSP Toolkit'; HandlerFn='Handle-CleanupSVSMSP'; Page='offboard' },
+ # Tweaks
+ @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' },
- # 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' },
+ @{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' },
+ @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' }
- # SVS Apps
- @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' },
- @{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' },
- @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' }
+ )
+Write-LogHybrid "Tasks by page: onboard=$(
+ ($Global:SamyTasks | Where-Object Page -eq 'onboard').Count
+) offboard=$(
+ ($Global:SamyTasks | Where-Object Page -eq 'offboard').Count
+) tweaks=$(
+ ($Global:SamyTasks | Where-Object Page -eq 'tweaks').Count
+) apps=$(
+ ($Global:SamyTasks | Where-Object Page -eq 'SVSApps').Count
+)" Info UI -LogToEvent
+
+ #endregion building the Menus
+
+ #region Build-Checkboxes
+ function Build-Checkboxes {
+ param(
+ [Parameter(Mandatory)][string]$Page,
+ [string]$Column
)
- Write-LogHybrid "Tasks by page: onboard=$(
- ($Global:SamyTasks | Where-Object Page -eq 'onboard').Count
- ) offboard=$(
- ($Global:SamyTasks | Where-Object Page -eq 'offboard').Count
- ) tweaks=$(
- ($Global:SamyTasks | Where-Object Page -eq 'tweaks').Count
- ) apps=$(
- ($Global:SamyTasks | Where-Object Page -eq 'SVSApps').Count
- )" Info UI -LogToEvent
+ # Start with all tasks on the given page
+ $tasks = $Global:SamyTasks | Where-Object Page -EQ $Page
- # =====================================================================
- # BUILD-CHECKBOXES
- # =====================================================================
- function Build-Checkboxes {
- param(
- [Parameter(Mandatory)][string]$Page,
- [string]$Column
- )
+ # Only filter by Column when it actually matters (onboard left/right)
+ if (-not [string]::IsNullOrEmpty($Column)) {
+ $tasks = $tasks | Where-Object Column -EQ $Column
+ }
- $tasks = $Global:SamyTasks | Where-Object Page -EQ $Page
+ (
+ $tasks |
+ ForEach-Object {
+ $taskId = $_.Id
+ $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
+ " title='$($_.Tooltip)'"
+ } else { '' }
- if (-not [string]::IsNullOrEmpty($Column)) {
- $tasks = $tasks | Where-Object Column -EQ $Column
- }
+ $html = ""
- (
- $tasks |
- ForEach-Object {
- $taskId = $_.Id
- $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
- " title='$($_.Tooltip)'"
- } else { '' }
+ if ($_.SubOptions) {
+ $subHtml = (
+ $_.SubOptions |
+ ForEach-Object {
+ ""
+ }
+ ) -join "`n"
- $html = ""
-
- if ($_.SubOptions) {
- $subHtml = (
- $_.SubOptions |
- ForEach-Object {
- ""
- }
- ) -join "`n"
-
- $html += @"
-
-$subHtml
-
+ $html += @"
+
+ $subHtml
+
"@
- }
-
- $html
}
- ) -join "`n"
- }
- # =====================================================================
- # MODULE VERSION HTML
- # =====================================================================
- function Get-ModuleVersionHtml {
- $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1
- if ($mod) {
- return "Module Version: $($mod.Version)
"
+ $html
}
- return "SVSMSP_Module not found
"
- }
+ ) -join "`n"
+ } # end function Build-checkboxes
- # =====================================================================
- # SERVER HELPERS
- # =====================================================================
- function Get-NextFreePort {
- param([int]$Start = $Port)
- for ($p = [Math]::Max(1024, $Start); $p -lt 65535; $p++) {
- $l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
- try { $l.Start(); $l.Stop(); return $p } catch {}
+
+ #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 "Module Version: $($mod.Version)
"
+ }
+ return "SVSMSP_Module not found
"
+ }
+
+ #endregion Get-ModuleVersionHtml
+
+ #region Strat-Server
+ function Get-NextFreePort {
+ param([int]$Start = $Port)
+ for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) {
+ $l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
+ try { $l.Start(); $l.Stop(); return $p } catch {}
+ }
+ throw "No free TCP port available."
+ }
+
+ # Starts the HTTP listener loop
+ function Start-Server {
+ $Global:Listener = [System.Net.HttpListener]::new()
+ $primaryPrefix = "http://localhost:$Port/"
+ $wildcardPrefix = "http://+:$Port/"
+
+ try {
+ $Global:Listener.Prefixes.Add($primaryPrefix)
+ $Global:Listener.Start()
+ Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent
+ }
+ catch [System.Net.HttpListenerException] {
+ if ($_.Exception.ErrorCode -eq 5) {
+ Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL…" Warning Server -LogToEvent
+ try {
+ $user = "$env:USERDOMAIN\$env:USERNAME"
+ if (-not $user.Trim()) { $user = $env:USERNAME }
+ Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait
+ $Global:Listener = [System.Net.HttpListener]::new()
+ $Global:Listener.Prefixes.Add($wildcardPrefix)
+ $Global:Listener.Start()
+ Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent
+ } catch {
+ Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent
+ return
}
- throw "No free TCP port available."
- }
-
- function Start-Server {
+ }
+ elseif ($_.Exception.NativeErrorCode -in 32,183) {
+ $old = $Port
+ $Port = Get-NextFreePort -Start ($Port + 1)
$Global:Listener = [System.Net.HttpListener]::new()
- $primaryPrefix = "http://localhost:$Port/"
- $wildcardPrefix = "http://+:$Port/"
-
- try {
- $Global:Listener.Prefixes.Add($primaryPrefix)
- $Global:Listener.Start()
- Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent
- }
- catch [System.Net.HttpListenerException] {
- if ($_.Exception.ErrorCode -eq 5) {
- Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL..." Warning Server -LogToEvent
- try {
- $user = "$env:USERDOMAIN\$env:USERNAME"
- if (-not $user.Trim()) { $user = $env:USERNAME }
- Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait
- $Global:Listener = [System.Net.HttpListener]::new()
- $Global:Listener.Prefixes.Add($wildcardPrefix)
- $Global:Listener.Start()
- Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent
- } catch {
- Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent
- return
- }
- }
- elseif ($_.Exception.NativeErrorCode -in 32,183) {
- $old = $Port
- $Port = Get-NextFreePort -Start ($Port + 1)
- $Global:Listener = [System.Net.HttpListener]::new()
- $primaryPrefix = "http://localhost:$Port/"
- $Global:Listener.Prefixes.Add($primaryPrefix)
- $Global:Listener.Start()
- Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent
- }
- else {
- Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent
- return
- }
- }
-
- try {
- while ($Global:Listener.IsListening) {
- $ctx = $Global:Listener.GetContext()
- try {
- Dispatch-Request $ctx
- } catch {
- Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent
- }
- }
- }
- finally {
- $Global:Listener.Close()
- Write-LogHybrid "Listener closed." Info Server -LogToEvent
- }
+ $primaryPrefix = "http://localhost:$Port/"
+ $Global:Listener.Prefixes.Add($primaryPrefix)
+ $Global:Listener.Start()
+ Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent
+ }
+ else {
+ Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent
+ return
+ }
}
- # =====================================================================
- # REMOTE TEXT FETCH (GITEA) + UI HTML
- # =====================================================================
- function Get-RemoteText {
- [CmdletBinding()]
- param(
- [Parameter(Mandatory = $true)]
- [string]$Url
- )
-
+ try {
+ while ($Global:Listener.IsListening) {
+ $ctx = $Global:Listener.GetContext()
try {
- $resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop
- return $resp.Content
- }
- catch {
- Write-LogHybrid "Get-RemoteText failed for $Url: $($_.Exception.Message)" Warning UI -LogToEvent
- return ""
+ Dispatch-Request $ctx
+ } catch {
+ Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent
}
+ }
}
+ finally {
+ $Global:Listener.Close()
+ Write-LogHybrid "Listener closed." Info Server -LogToEvent
+ }
+ }
+#endregion Strat-Server
- function Get-UIHtml {
- param([string]$Page = 'onboard')
- if (-not $Page) { $Page = 'onboard' }
- $onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left'
- $onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right'
- $offboard = Build-Checkboxes -Page 'offboard' -Column ''
- $tweaks = Build-Checkboxes -Page 'tweaks' -Column ''
- $apps = Build-Checkboxes -Page 'SVSApps' -Column ''
+#region UIHtml
- $tasksJsAll = (
- $Global:SamyTasks | ForEach-Object {
- " { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
- }
- ) -join ",`n"
+function Get-UIHtml {
+ param([string]$Page = 'onboard')
+ if (-not $Page) { $Page = 'onboard' }
- $styleContent = Get-RemoteText -Url "https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/samy.css"
- $scriptContent = Get-RemoteText -Url "https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/samy.js"
+ #
+ # 1) Build checkbox HTML per page/column
+ #
+ $onboardLeft = Build-Checkboxes -Page 'onboard' -Column 'left'
+ $onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right'
+ $offboard = Build-Checkboxes -Page 'offboard' -Column ''
+ $tweaks = Build-Checkboxes -Page 'tweaks' -Column ''
+ $apps = Build-Checkboxes -Page 'SVSApps' -Column ''
- $htmlTemplate = @"
+ #
+ # 2) Build the JS tasks array once (this is the only dynamic JS piece)
+ #
+ $tasksJsAll = (
+ $Global:SamyTasks | ForEach-Object {
+ " { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
+ }
+ ) -join ",`n"
+
+ #
+ # 3) HTML template that references external CSS/JS hosted on Gitea
+ # (adjust the URLs to match your repo + branch)
+ #
+ $htmlTemplate = @"
@@ -802,17 +1070,18 @@ $subHtml
Script Monkey
-
+
+
+

{{moduleVersion}}
+
Script Automation Monkey (Yeah!)
@@ -859,7 +1128,7 @@ $styleContent
-
+
Off-Boarding
@@ -897,16 +1166,19 @@ $styleContent
+
+
+
+
+
@@ -916,550 +1188,730 @@ $scriptContent
"@
- $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)
+ #
+ # 4) Replace placeholders
+ #
+ $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
+ 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."
+ }
+}
+
+function Handle-InstallChrome { param($Context)
+ try {
+ winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
+ Write-LogHybrid "Installed Google Chrome via winget" Success SVSApps -LogToEvent
+ Respond-Text $Context "Chrome installed"
+ } catch {
+ Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+function Handle-InstallAcrobat { param($Context)
+ try {
+ winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
+ Write-LogHybrid "Installed Adobe Acrobat Reader (64-bit) via winget" Success SVSApps -LogToEvent
+ Respond-Text $Context "Acrobat Reader installed"
+ } catch {
+ Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+ #Offboarding Handlers
+ function Handle-UninstallCyberQP {
+ param($Context)
+
+ try {
+ if (Get-Command Uninstall-CyberQP -ErrorAction Stop) {
+ Uninstall-CyberQP
+ Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent
+ Respond-Text $Context "CyberQP uninstalled."
+ } else {
+ throw "Uninstall-CyberQP cmdlet not found in SVSMSP toolkit."
+ }
+ }
+ catch {
+ Write-LogHybrid "Error uninstalling CyberQP: $($_.Exception.Message)" Error OffBoard -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+function Handle-UninstallSVSHelpDesk {
+ param($Context)
+
+ try {
+ if (Get-Command Uninstall-SVSHelpDesk -ErrorAction Stop) {
+ Uninstall-SVSHelpDesk
+ Write-LogHybrid "SVS HelpDesk uninstalled" Success OffBoard -LogToEvent
+ Respond-Text $Context "SVS HelpDesk uninstalled."
+ } else {
+ throw "Uninstall-SVSHelpDesk cmdlet not found in SVSMSP toolkit."
+ }
+ }
+ catch {
+ Write-LogHybrid "Error uninstalling SVS HelpDesk: $($_.Exception.Message)" Error OffBoard -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+function Handle-UninstallThreatLocker {
+ param($Context)
+
+ try {
+ if (Get-Command Uninstall-ThreatLocker -ErrorAction Stop) {
+ Uninstall-ThreatLocker
+ Write-LogHybrid "ThreatLocker uninstalled" Success OffBoard -LogToEvent
+ Respond-Text $Context "ThreatLocker uninstalled."
+ } else {
+ throw "Uninstall-ThreatLocker cmdlet not found in SVSMSP toolkit."
+ }
+ }
+ catch {
+ Write-LogHybrid "Error uninstalling ThreatLocker: $($_.Exception.Message)" Error OffBoard -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+function Handle-UninstallRocketCyber {
+ param($Context)
+
+ try {
+ if (Get-Command Uninstall-RocketCyber -ErrorAction Stop) {
+ Uninstall-RocketCyber
+ Write-LogHybrid "RocketCyber uninstalled" Success OffBoard -LogToEvent
+ Respond-Text $Context "RocketCyber uninstalled."
+ } else {
+ throw "Uninstall-RocketCyber cmdlet not found in SVSMSP toolkit."
+ }
+ }
+ catch {
+ Write-LogHybrid "Error uninstalling RocketCyber: $($_.Exception.Message)" Error OffBoard -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+function Handle-CleanupSVSMSP {
+ param($Context)
+
+ try {
+ if (Get-Command Install-SVSMSP -ErrorAction Stop) {
+ # This will:
+ # - Uninstall SVSMSP
+ # - Unregister SVS_Repo
+ # - Remove SVSMSP from the session
+ # - Delete HKLM:\Software\SVS\Deployment (via Remove-SVSDeploymentRegKey)
+ Install-SVSMSP -Cleanup
+
+ Write-LogHybrid "SVSMSP toolkit cleanup complete (module, repo, registry)." Success OffBoard -LogToEvent
+ Respond-Text $Context "SVSMSP toolkit cleanup complete."
+ } else {
+ throw "Install-SVSMSP function not found in current session."
+ }
+ }
+ catch {
+ Write-LogHybrid "Error cleaning up SVSMSP toolkit: $($_.Exception.Message)" Error OffBoard -LogToEvent
+ Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ }
+}
+
+
+
+ #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
}
- # =====================================================================
- # HTTP HELPERS AND HANDLERS
- # =====================================================================
- 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()
+ # 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
+ }
}
- function Respond-HTML {
- [CmdletBinding()]
- param(
- [Parameter(Mandatory = $true)][object]$Context,
- [Parameter(Mandatory = $true)][string]$Html
+ # 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)
)
- $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()
+ $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" }
- 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)
-
+ # 4) Fetch site list only
+ if ($FetchSites) {
try {
- $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
- $pw = (ConvertFrom-Json $raw).password
+ $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
- $Global:WebhookPassword = $pw
+ 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
+ }
- $sites = Install-DattoRMM `
- -UseWebhook `
- -WebhookPassword $pw `
- -FetchSites `
- -SaveSitesList:$SaveSitesList `
- -OutputFile $OutputFile
-
- Respond-JSON $Context $sites
- }
- catch {
- Write-LogHybrid "Handle-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- $Context.Response.StatusCode = 500
- Respond-Text $Context "Internal server error fetching sites."
- }
- }
-
- function Handle-SetSVSPowerPlan {
- param($Context)
- Set-SVSPowerPlan
- 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."
+ return $siteList
} catch {
- Write-LogHybrid "Error in Install-SVSMSP: $_" Error OnBoard
- Respond-Text $Context "ERROR: $_"
+ Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM -LogToEvent; return @()
}
}
- function Handle-InstallCyberQP {
- param($Context)
- Install-CyberQP
- Write-LogHybrid "CyberQP installed" Success OnBoard
- Respond-Text $Context "CyberQP installed"
- }
-
- function Handle-InstallThreatLocker {
- param($Context)
- Install-ThreatLocker
- Write-LogHybrid "ThreatLocker installed" Success OnBoard
- Respond-Text $Context "ThreatLocker installed"
- }
-
- function Handle-InstallRocketCyber {
- param($Context)
- Install-RocketCyber
- Write-LogHybrid "RocketCyber installed" Success OnBoard
- Respond-Text $Context "RocketCyber installed"
- }
-
- function Handle-InstallSVSHelpDesk {
- param($Context)
- Install-SVSHelpDesk
- Write-LogHybrid "SVS HelpDesk installed" Success OnBoard
- Respond-Text $Context "SVS HelpDesk installed"
- }
-
- function Handle-InstallDattoRMM {
- param($Context)
-
+ # 5) Push site variables to registry
+ if ($PushSiteVars) {
try {
- if ($Context.Request.HttpMethod -ne 'POST') {
- $Context.Response.StatusCode = 405
- Respond-Text $Context 'Use POST'
- return
- }
-
- $body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
- $data = ConvertFrom-Json $body
-
- 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')
-
- Respond-Text $Context "Triggered DattoRMM for $($data.Name)"
- }
- catch {
- Write-LogHybrid "Handle-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- $Context.Response.StatusCode = 500
- Respond-Text $Context "Internal server error during DattoRMM install."
- }
- }
-
- function Handle-InstallChrome {
- param($Context)
- try {
- winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements
- Write-LogHybrid "Installed Google Chrome via winget" Success SVSApps -LogToEvent
- Respond-Text $Context "Chrome installed"
+ $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 "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
+ Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
- }
-
- function Handle-InstallAcrobat {
- param($Context)
- try {
- winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements
- Write-LogHybrid "Installed Adobe Acrobat Reader (64-bit) via winget" Success SVSApps -LogToEvent
- Respond-Text $Context "Acrobat Reader installed"
- } catch {
- Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
- }
- }
-
- function Handle-UninstallCyberQP {
- param($Context)
-
- try {
- if (Get-Command Uninstall-CyberQP -ErrorAction Stop) {
- Uninstall-CyberQP
- Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent
- Respond-Text $Context "CyberQP uninstalled."
- } else {
- throw "Uninstall-CyberQP cmdlet not found in SVSMSP toolkit."
- }
- }
- catch {
- Write-LogHybrid "Error uninstalling CyberQP: $($_.Exception.Message)" Error OffBoard -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
- }
- }
-
- function Handle-UninstallSVSHelpDesk {
- param($Context)
-
- try {
- if (Get-Command Uninstall-SVSHelpDesk -ErrorAction Stop) {
- Uninstall-SVSHelpDesk
- Write-LogHybrid "SVS HelpDesk uninstalled" Success OffBoard -LogToEvent
- Respond-Text $Context "SVS HelpDesk uninstalled."
- } else {
- throw "Uninstall-SVSHelpDesk cmdlet not found in SVSMSP toolkit."
- }
- }
- catch {
- Write-LogHybrid "Error uninstalling SVS HelpDesk: $($_.Exception.Message)" Error OffBoard -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
- }
- }
-
- function Handle-UninstallThreatLocker {
- param($Context)
-
- try {
- if (Get-Command Uninstall-ThreatLocker -ErrorAction Stop) {
- Uninstall-ThreatLocker
- Write-LogHybrid "ThreatLocker uninstalled" Success OffBoard -LogToEvent
- Respond-Text $Context "ThreatLocker uninstalled."
- } else {
- throw "Uninstall-ThreatLocker cmdlet not found in SVSMSP toolkit."
- }
- }
- catch {
- Write-LogHybrid "Error uninstalling ThreatLocker: $($_.Exception.Message)" Error OffBoard -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
- }
- }
-
- function Handle-UninstallRocketCyber {
- param($Context)
-
- try {
- if (Get-Command Uninstall-RocketCyber -ErrorAction Stop) {
- Uninstall-RocketCyber
- Write-LogHybrid "RocketCyber uninstalled" Success OffBoard -LogToEvent
- Respond-Text $Context "RocketCyber uninstalled."
- } else {
- throw "Uninstall-RocketCyber cmdlet not found in SVSMSP toolkit."
- }
- }
- catch {
- Write-LogHybrid "Error uninstalling RocketCyber: $($_.Exception.Message)" Error OffBoard -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
- }
- }
-
- function Handle-CleanupSVSMSP {
- param($Context)
-
- try {
- if (Get-Command Install-SVSMSP -ErrorAction Stop) {
- Install-SVSMSP -Cleanup
- Write-LogHybrid "SVSMSP toolkit cleanup complete (module, repo, registry)." Success OffBoard -LogToEvent
- Respond-Text $Context "SVSMSP toolkit cleanup complete."
- } else {
- throw "Install-SVSMSP function not found in current session."
- }
- }
- catch {
- Write-LogHybrid "Error cleaning up SVSMSP toolkit: $($_.Exception.Message)" Error OffBoard -LogToEvent
- Respond-Text $Context "ERROR: $($_.Exception.Message)"
- }
- }
-
- # =====================================================================
- # INSTALL-DattoRMM
- # =====================================================================
- 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
- )
-
- if ($SaveSitesList -and -not $FetchSites) {
- Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM -LogToEvent
- return
- }
-
- if ($UseWebhook) {
- if (-not $WebhookPassword) {
- Write-LogHybrid "Webhook password missing." Error DattoRMM -LogToEvent
- return
- }
+ $regPath = "HKLM:\Software\SVS\Deployment"
+ foreach ($v in $varsResp.variables) {
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
+ 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 fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- return
+ Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
}
+ }
- if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
- Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent
- return
- }
-
- [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" }
-
- 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 @()
- }
- }
-
- 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
- }
- }
- }
-
- 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
- }
- }
- }
-
- if ($SaveCopy) {
+ # 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"
- $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
+ $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 "Save-copy failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
+ Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
}
}
+ }
- if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
- Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning 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
}
}
- # =====================================================================
- # DISPATCH-REQUEST
- # =====================================================================
- function Dispatch-Request {
- param($Context)
-
- $path = $Context.Request.Url.AbsolutePath.TrimStart('/')
-
- if ($path -eq 'quit') {
- Write-LogHybrid "Shutdown requested" Info Server -LogToEvent
- Respond-Text $Context "Server shutting down."
- $Global:Listener.Stop()
- return
- }
-
- if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
- Handle-FetchSites $Context
- return
- }
-
- 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 = $Global:SamyTasks | Where-Object Name -EQ $path
- if ($task) {
- & $task.HandlerFn $Context
- return
- }
-
- $Context.Response.StatusCode = 404
- Respond-Text $Context '404 - Not Found'
+ # 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:SamyTasks | 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)
+ # ─────────────────────────────────────────────────────────────────────────
- # =====================================================================
- # MAIN ENTRY LOGIC (PARAMETER SET SWITCH)
- # =====================================================================
switch ($PSCmdlet.ParameterSetName) {
- 'Toolkit' {
- Write-LogHybrid "Toolkit-only mode" Info Startup -LogToEvent
- Install-SVSMSP -InstallToolkit
- return
- }
+ '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
- }
+ '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
+ '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' {
+ $url = "http://localhost:$Port/"
+ Write-LogHybrid "Starting ScriptMonkey UI on $url" Info Startup
+
+ # Open the UI in a separate PowerShell job so Start-Server can block safely.
+ try {
+ Start-Job -Name 'OpenScriptMonkeyUI' -ScriptBlock {
+ param($u)
+ Start-Sleep -Milliseconds 300
+ try {
+ if (Get-Command -Name 'msedge.exe' -ErrorAction SilentlyContinue) {
+ Start-Process -FilePath 'msedge.exe' -ArgumentList "--app=$u"
+ } else {
+ Start-Process -FilePath $u
+ }
+ } catch { }
+ } -ArgumentList $url | Out-Null
+ } catch {
+ Write-LogHybrid "Failed to schedule browser launch: $($_.Exception.Message)" Warning Startup -LogToEvent
}
- 'DattoInstall' {
- Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth -LogToEvent
+ # Now start the blocking listener loop
+ Start-Server
+ return
+ }
- if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) {
- Install-DattoRMM `
- -UseWebhook `
- -WebhookPassword $WebhookPassword `
- -SiteUID $SiteUID `
- -SiteName $SiteName `
- -PushSiteVars:$PushSiteVars `
- -InstallRMM:$InstallRMM `
- -SaveCopy:$SaveCopy
+#>
+
+ 'UI' {
+ $url = "http://localhost:$Port/"
+ Write-LogHybrid "Starting ScriptMonkey UI on $url" Info Startup
+
+ # Resolve Edge path explicitly (x86 first, then 64-bit, then PATH)
+ $edgeCandidates = @(
+ "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
+ "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe"
+ )
+ $edgePath = $edgeCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
+ if (-not $edgePath) {
+ $cmd = Get-Command -Name 'msedge.exe' -ErrorAction SilentlyContinue
+ if ($cmd) { $edgePath = $cmd.Path }
+ }
+
+ # Launch Edge (app mode) in a background job so Start-Server can block
+ Start-Job -Name 'OpenScriptMonkeyUI' -ScriptBlock {
+ param([string]$u, [string]$edge)
+ Start-Sleep -Milliseconds 400
+ try {
+ if ($edge -and (Test-Path $edge)) {
+ Start-Process -FilePath $edge -ArgumentList @('--new-window', "--app=$u")
+ } else {
+ Start-Process -FilePath $u # fallback to default browser
}
+ } catch { }
+ } -ArgumentList $url, $edgePath | Out-Null
- return
- }
+ # Now start the blocking listener loop
+ Start-Server
+ return
+ }
- 'UI' {
- $url = "http://localhost:$Port/"
- Write-LogHybrid "Starting ScriptMonkey UI on $url" Info Startup
- $edgeCandidates = @(
- "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe",
- "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe"
- )
- $edgePath = $edgeCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1
- if (-not $edgePath) {
- $cmd = Get-Command -Name 'msedge.exe' -ErrorAction SilentlyContinue
- if ($cmd) { $edgePath = $cmd.Path }
- }
- Start-Job -Name 'OpenScriptMonkeyUI' -ScriptBlock {
- param([string]$u, [string]$edge)
- Start-Sleep -Milliseconds 400
- try {
- if ($edge -and (Test-Path $edge)) {
- Start-Process -FilePath $edge -ArgumentList @('--new-window', "--app=$u")
- } else {
- Start-Process -FilePath $u
- }
- } catch { }
- } -ArgumentList $url, $edgePath | Out-Null
-
- Start-Server
- return
- }
}
-}
+ #endregion EntryPoint: Define Invoke-ScriptMonkey
-# =====================================================================
-# ENTRYPOINT WRAPPER
-# =====================================================================
-if ($MyInvocation.InvocationName -eq '.') {
- # dot-sourced, do not auto-run
-}
-elseif ($PSCommandPath) {
+
+ }
+
+ if ($MyInvocation.InvocationName -eq '.') {
+ # dot-sourced, don't invoke
+} elseif ($PSCommandPath) {
+ # script was saved and run directly
Invoke-ScriptMonkey @PSBoundParameters
-}
-else {
- if ($args.Count -gt 0) {
+} 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('-')
+ $key = $args[$i].TrimStart('-')
$next = $args[$i + 1]
if ($next -and ($next -notlike '-*')) {
$namedArgs[$key] = $next
- $i++
+ $i++ # Skip next one, it's the value
} else {
$namedArgs[$key] = $true
}
@@ -1470,3 +1922,5 @@ else {
Invoke-ScriptMonkey
}
}
+
+