From 2e3890b5ca096854c13b53e0ff001adfd0a40f5c Mon Sep 17 00:00:00 2001 From: Stephan Yelle Date: Wed, 26 Nov 2025 19:52:37 -0500 Subject: [PATCH] Update samy.ps1 --- samy.ps1 | 2156 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 1305 insertions(+), 851 deletions(-) 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 = " $($_.Label)" - ( - $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 = " $($_.Label)" - - if ($_.SubOptions) { - $subHtml = ( - $_.SubOptions | - ForEach-Object { - "" - } - ) -join "`n" - - $html += @" - + $html += @" + "@ - } - - $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 - + +
+
SVS Logo {{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 } } + +