1949 lines
77 KiB
PowerShell
1949 lines
77 KiB
PowerShell
#region changes to be done
|
||
|
||
#endregion changes to be done
|
||
|
||
## Last changes made should fix the issues we had wen running thi in Windows 11 25H2
|
||
|
||
<#
|
||
.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 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
|
||
}
|
||
}
|
||
|
||
# ─── 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)" Warning 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)" Warning 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
|
||
}
|
||
|
||
|
||
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 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
|
||
}
|
||
}
|
||
|
||
|
||
|
||
function Perform-ToolkitInstallation {
|
||
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 "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
|
||
}
|
||
|
||
#endregion SVS Module
|
||
|
||
|
||
|
||
#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,
|
||
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
|
||
Default Source : SVSMSP_Module
|
||
#>
|
||
[CmdletBinding()]
|
||
param (
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$Message,
|
||
|
||
[ValidateSet("Info", "Warning", "Error", "Success", "General")]
|
||
[string]$Level = "Info",
|
||
|
||
[string]$TaskCategory = "GeneralTask",
|
||
|
||
[switch]$LogToEvent = $false,
|
||
|
||
[string]$EventSource = "SVSMSP_Module",
|
||
|
||
# Custom log name so you get your own node under "Applications and Services Logs"
|
||
[string]$EventLog = "SVSMSP Events",
|
||
|
||
[int]$CustomEventID,
|
||
|
||
[string]$LogFile,
|
||
|
||
[switch]$PassThru
|
||
)
|
||
|
||
# ---------- Event ID / console color ----------
|
||
$EventID = if ($CustomEventID) { $CustomEventID } else {
|
||
switch ($Level) {
|
||
"Info" { 1000 }
|
||
"Warning" { 2000 }
|
||
"Error" { 3000 }
|
||
"Success" { 4000 }
|
||
default { 1000 }
|
||
}
|
||
}
|
||
|
||
$Color = switch ($Level) {
|
||
"Info" { "Cyan" }
|
||
"Warning" { "Yellow" }
|
||
"Error" { "Red" }
|
||
"Success" { "Green" }
|
||
default { "White" }
|
||
}
|
||
|
||
$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()
|
||
}
|
||
|
||
$logEntry = [PSCustomObject]@{
|
||
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
||
Level = $Level
|
||
Message = $FormattedMessage
|
||
}
|
||
[void]$Global:LogCache.Add($logEntry)
|
||
|
||
# ---------- Optional file output ----------
|
||
if ($LogFile) {
|
||
try {
|
||
"$($logEntry.Timestamp) $FormattedMessage" |
|
||
Out-File -FilePath $LogFile -Append -Encoding UTF8
|
||
}
|
||
catch {
|
||
Write-Host "[Warning] Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
|
||
# ---------- 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 = @{}
|
||
}
|
||
|
||
$EntryType = switch ($Level) {
|
||
"Info" { "Information" }
|
||
"Warning" { "Warning" }
|
||
"Error" { "Error" }
|
||
"Success" { "Information" } # treat success as info in Event Log
|
||
default { "Information" }
|
||
}
|
||
|
||
$sourceKey = "$EventLog|$EventSource"
|
||
|
||
if (-not $Global:EventSourceInitState.ContainsKey($sourceKey) -or
|
||
-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()
|
||
$principal = New-Object Security.Principal.WindowsPrincipal($current)
|
||
$isAdmin = $principal.IsInRole(
|
||
[Security.Principal.WindowsBuiltInRole]::Administrator
|
||
)
|
||
}
|
||
catch {
|
||
$isAdmin = $false
|
||
}
|
||
|
||
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'
|
||
}
|
||
"@
|
||
|
||
$tempPath = [System.IO.Path]::Combine(
|
||
$env:TEMP,
|
||
"Init_${EventLog}_$EventSource.ps1".Replace(' ', '_')
|
||
)
|
||
|
||
$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
|
||
}
|
||
catch {
|
||
Write-Host "[Warning] Auto-elevation to create Event Log '$EventLog' / source '$EventSource' failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
||
}
|
||
finally {
|
||
Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
}
|
||
|
||
# Re-check after creation attempt
|
||
if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
|
||
$Global:EventSourceInitState[$sourceKey] = $true
|
||
}
|
||
else {
|
||
$Global:EventSourceInitState[$sourceKey] = $false
|
||
Write-Host "[Warning] Event source '$EventSource' does not exist and could not be created. Skipping Event Log write." -ForegroundColor Yellow
|
||
}
|
||
}
|
||
catch {
|
||
Write-Host "[Warning] Failed to initialize Event Log '$EventLog' / source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow
|
||
$Global:EventSourceInitState[$sourceKey] = $false
|
||
}
|
||
}
|
||
|
||
# Only write if initialization succeeded
|
||
if ($Global:EventSourceInitState[$sourceKey]) {
|
||
try {
|
||
$EventMessage = "TaskCategory: $TaskCategory | Message: $Message"
|
||
Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $EntryType -EventId $EventID -Message $EventMessage
|
||
}
|
||
catch {
|
||
Write-Host "[Warning] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow
|
||
}
|
||
}
|
||
}
|
||
# ------------------------------------------------------------------------------------------
|
||
|
||
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(
|
||
[Parameter(Mandatory = $true)]
|
||
[string]$Message,
|
||
|
||
[ValidateSet("Info", "Warning", "Error", "Success", "General")]
|
||
[string]$Level = "Info",
|
||
|
||
[string]$TaskCategory = "GeneralTask",
|
||
|
||
[switch]$LogToEvent,
|
||
|
||
[string]$EventSource = "SVSMSP_Module",
|
||
|
||
[string]$EventLog = "SVSMSP Events",
|
||
|
||
[int]$CustomEventID,
|
||
|
||
[string]$LogFile,
|
||
|
||
[switch]$PassThru,
|
||
|
||
[ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")]
|
||
[string]$ForegroundColorOverride
|
||
)
|
||
|
||
$formatted = "[$Level] [$TaskCategory] $Message"
|
||
|
||
# Build the common parameter set for forwarding into Write-Log / Write-LogHelper
|
||
$invokeParams = @{
|
||
Message = $Message
|
||
Level = $Level
|
||
TaskCategory = $TaskCategory
|
||
LogToEvent = $LogToEvent
|
||
EventSource = $EventSource
|
||
EventLog = $EventLog
|
||
}
|
||
|
||
if ($PSBoundParameters.ContainsKey('CustomEventID')) {
|
||
$invokeParams.CustomEventID = $CustomEventID
|
||
}
|
||
if ($PSBoundParameters.ContainsKey('LogFile')) {
|
||
$invokeParams.LogFile = $LogFile
|
||
}
|
||
if ($PassThru) {
|
||
$invokeParams.PassThru = $true
|
||
}
|
||
|
||
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
|
||
}
|
||
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 @invokeParams
|
||
}
|
||
else {
|
||
Write-LogHelper @invokeParams
|
||
}
|
||
}
|
||
}
|
||
|
||
#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='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='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' },
|
||
|
||
# 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
|
||
)
|
||
|
||
# Start with all tasks on the given page
|
||
$tasks = $Global:SamyTasks | Where-Object Page -EQ $Page
|
||
|
||
# Only filter by Column when it actually matters (onboard left/right)
|
||
if (-not [string]::IsNullOrEmpty($Column)) {
|
||
$tasks = $tasks | Where-Object Column -EQ $Column
|
||
}
|
||
|
||
(
|
||
$tasks |
|
||
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) {
|
||
$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"
|
||
} # end function Build-checkboxes
|
||
|
||
|
||
#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
|
||
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
|
||
}
|
||
}
|
||
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
|
||
}
|
||
}
|
||
#endregion Strat-Server
|
||
|
||
#region UIHtml
|
||
|
||
function Get-RemoteText {
|
||
[CmdletBinding()]
|
||
param(
|
||
[Parameter(Mandatory = $true)][string]$Url
|
||
)
|
||
|
||
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 ""
|
||
}
|
||
}
|
||
|
||
function Get-UIHtml {
|
||
param([string]$Page = 'onboard')
|
||
if (-not $Page) { $Page = 'onboard' }
|
||
|
||
#
|
||
# 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 ''
|
||
|
||
#
|
||
# 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) Pull CSS/JS from Gitea and inline them
|
||
#
|
||
$cssContent = Get-RemoteText -Url "https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/samy.css?raw=1"
|
||
$jsContent = Get-RemoteText -Url "https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/samy.js?raw=1"
|
||
|
||
#
|
||
# 4) HTML template – **no external link/script src** anymore, all inlined
|
||
#
|
||
$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.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/SVS_Favicon.ico">
|
||
|
||
<style>
|
||
$cssContent
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="logo-container">
|
||
<!-- SVS Logo (left) -->
|
||
<div class="logo-left">
|
||
<img src="https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/SVS_logo.svg" alt="SVS Logo">
|
||
{{moduleVersion}}
|
||
</div>
|
||
|
||
<!-- Centered rotating tagline -->
|
||
<div id="tagline" class="tagline">
|
||
Script Automation Monkey (Yeah!)
|
||
</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 id="status-box" style="margin-top: 1em; font-family: monospace;"></div>
|
||
</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>
|
||
|
||
<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>
|
||
|
||
<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()" class="go-button">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> <!-- end onboardTab -->
|
||
|
||
<div id="offboardTab" class="tab-content">
|
||
<h2>Off-Boarding</h2>
|
||
<div class="columns-container">
|
||
<div class="checkbox-group column">
|
||
<h3>Remove Stack</h3>
|
||
<label>
|
||
<input type="checkbox" id="offboardSelectAll" onclick="toggleOffboardAll()">
|
||
Select All
|
||
</label>
|
||
{{offboardCheckboxes}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="tweaksTab" class="tab-content">
|
||
<h2>Tweaks</h2>
|
||
<div class="columns-container">
|
||
<div class="checkbox-group column">
|
||
<h3>Tweaks</h3>
|
||
{{tweaksCheckboxes}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div id="SVSAppsTab" class="tab-content">
|
||
<h2>SVS APPs</h2>
|
||
<div class="columns-container">
|
||
<div class="checkbox-group column">
|
||
<h3>Applications</h3>
|
||
{{appsCheckboxes}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Tiny inline bridge: pass dynamic data, then inline JS from Gitea -->
|
||
<script>
|
||
window.SAMY_TASKS = [
|
||
{{tasksJsAll}}
|
||
];
|
||
|
||
window.SAMY_DEFAULT_PAGE = "{{defaultPage}}";
|
||
</script>
|
||
|
||
<script>
|
||
$jsContent
|
||
</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>
|
||
"@
|
||
|
||
#
|
||
# 5) Replace placeholders (unchanged vs your version)
|
||
#
|
||
$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."
|
||
}
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
# 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: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)
|
||
# ─────────────────────────────────────────────────────────────────────────
|
||
|
||
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' {
|
||
$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
|
||
}
|
||
|
||
# Now start the blocking listener loop
|
||
Start-Server
|
||
return
|
||
}
|
||
|
||
#>
|
||
|
||
'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
|
||
|
||
# Now start the blocking listener loop
|
||
Start-Server
|
||
return
|
||
}
|
||
|
||
|
||
|
||
}
|
||
#endregion EntryPoint: Define Invoke-ScriptMonkey
|
||
|
||
|
||
}
|
||
|
||
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
|
||
}
|
||
}
|
||
|
||
|