diff --git a/samy.old.ps1 b/samy.old.ps1 deleted file mode 100644 index 4a051d2..0000000 --- a/samy.old.ps1 +++ /dev/null @@ -1,3069 +0,0 @@ -<# -Last changes made should fix the issues we had wen running thi in Windows 11 25H2 - -change this line for the message - -
Please use samy.svstools.ca
- -Invoke-installprinters you can uncomment whatif in the function - -# SAMY asset config (change branch or base once and it updates everything) -$Script:SamyBranch = 'beta' # or 'main' - -.SYNOPSIS - Script Automation Monkey (SAMY) is a unified MSP assistant that automates onboarding, headless offboarding, - Datto RMM deployments, and toolkit management through a local UI, HTTP endpoints, or direct PowerShell switches. - -.DESCRIPTION - Install-DattoRMM is the core helper that handles credential retrieval (webhook or direct), OAuth token management, - site list fetching/persistence, registry variable pushes, agent downloads/installs, and optional installer archiving. - The UI exposes all tasks as checkboxes with select-all helpers, and each action is wrapped in detailed logging plus - HTTP responders so callers can see success or failure. - - - Key features: - - Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook when requested. - - OAuth management - automatically acquires and refreshes bearer tokens over TLS to talk to Datto. - - Site list fetching - returns the list of RMM sites and validates OutputFile extensions (.csv/.json). - - Site list persistence - saves fetched site lists to the requester's desktop as CSV or JSON. - - Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment. - - Agent download & install - pulls the Datto RMM agent, launches it, and optionally saves a copy to C:\Temp. - - HTTP endpoints - /getpw and /installDattoRMM handle UI or API-triggered workflows with failure trapping. - - Idempotent & WhatIf support - uses ShouldProcess/SupportsShouldProcess to protect installs. - - Headless Offboard - runs every offboarding task serially just like "Select All" in the UI. - - 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 Offboard - Switch that runs every off-boarding task sequentially (same behavior as checking "Select All" on the Off-Boarding tab) without launching the web UI. - -.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. - -.NOTES - Default EventLog : SVSMSP Events - Default Source : SAMY - -.EXAMPLE - - & ([ScriptBlock]::Create( (iwr 'https://samy.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://samy.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://samy.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://samy.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://samy.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 'samy.svstools.ca').Content )) -SilentInstall - -.EXAMPLE - & ([ScriptBlock]::Create((iwr 'samy.svstools.com').Content)) -Cleanup - -.EXAMPLE -& ([ScriptBlock]::Create((iwr 'samy.svstools.ca').Content)) -Offboard - - # Runs the off-boarding tasks in sequence without launching the UI. - -#> -#region Safely bypass Restricted Execution Policy -# ─── Safely bypass Restricted Execution Policy ─── -#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 - - # Rebuild the original argument list as a string to pass through - $argList = @() - foreach ($a in $args) { - if ($a -is [string]) { - # Quote and escape any existing quotes - $escaped = $a.Replace('"','`"') - $argList += "`"$escaped`"" - } else { - $argList += $a.ToString() - } - } - $argString = $argList -join ' ' - - if ($PSCommandPath) { - # Script saved on disk: re-run same file with same args - powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`"" $argString - } else { - # iwr | iex scenario: re-download SAMY and apply same args - powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://samy.svstools.ca' -UseBasicParsing | iex } $argString" - } - - exit -} - -# TLS and silent install defaults -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -$ProgressPreference = 'SilentlyContinue' -$ConfirmPreference = 'None' -#endregion Safely bypass Restricted Execution Policy - -#endregion Safely bypass Restricted Execution Policy - - function Invoke-ScriptAutomationMonkey { - - # ───────────────────────────────────────────────────────────────────────── - # PARAMETERS + GLOBAL VARIABLES - # ───────────────────────────────────────────────────────────────────────── - - [CmdletBinding( - DefaultParameterSetName='UI', - SupportsShouldProcess=$true, - ConfirmImpact= 'Medium' - )] - #region Parameter Definitions - param( - # ───────────────────────────────────────────────────────── - # Toolkit-only mode - [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, - - # ───────────────────────────────────────────────────────── - # remove Toolkit - [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, - - # headless offboarding - [Parameter(Mandatory,ParameterSetName='Offboard')][switch]$Offboard, - - # ───────────────────────────────────────────────────────── - # 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')] - [AllowEmptyString()] - [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 - ) - #endregion Parameter Definitions - - #region global variables - - # Listening port for HTTP UI - $Port = 8082 - - # Configurable endpoints - $Global:DattoWebhookUrl = 'https://bananas.svstools.ca/dattormm' - - # SAMY asset config (change branch or base once and it updates everything) - $Script:SamyBranch = 'beta' # 'main' or 'beta' - $Script:SamyRepoBase = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch' - - - # Top-left corner logo (SVS) - $Script:SamyTopLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_logo.svg" - - # Background SAMY image used in CSS - $Script:SamyBgLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SAMY.png" - - $Script:SamyFaviconUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_Favicon.ico" - $Script:SamyCssUrl = "$Script:SamyRepoBase/$Script:SamyBranch/samy.css?raw=1" - $Script:SamyJsUrl = "$Script:SamyRepoBase/$Script:SamyBranch/samy.js?raw=1" - - - # 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 Start-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 - } - } - # CSCE cleanup - $cscePath = 'C:\CSCE' - if (Test-Path $cscePath) { - try { - Remove-Item -Path $cscePath -Recurse -Force - Write-LogHybrid "Deleted '$cscePath' contents." "Success" "SVSModule" -LogToEvent - } catch { - Write-LogHybrid "Failed to delete '$cscePath': $($_.Exception.Message)" "Warning" "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 Repair-SVSMspEventLogBinding { - param( - [string]$EventSource = "SVSMSP_Module", - [string]$TargetLog = "SVSMSP Events" - ) - - Write-LogHybrid "Checking Event Log binding for source '$EventSource'..." Info SVSModule -LogToEvent - - # 1) Make sure the source exists - try { - if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { - Write-LogHybrid "Event source '$EventSource' not found. Nothing to repair." Info SVSModule -LogToEvent - return - } - - $currentLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($EventSource, '.') - } - catch { - Write-LogHybrid "Failed to query Event Log binding for '$EventSource': $($_.Exception.Message)" Warning SVSModule -LogToEvent - return - } - - if (-not $currentLog) { - Write-LogHybrid "Could not determine current log for event source '$EventSource'. Skipping repair." Warning SVSModule -LogToEvent - return - } - - # 2) If it's already correct, bail out - if ($currentLog -eq $TargetLog) { - Write-LogHybrid "Event source '$EventSource' already bound to '$TargetLog'." Info SVSModule -LogToEvent - return - } - - Write-LogHybrid "Rebinding event source '$EventSource' from '$currentLog' to '$TargetLog'..." Warning SVSModule -LogToEvent - - # 3) Delete and recreate the source bound to the desired log - try { - [System.Diagnostics.EventLog]::DeleteEventSource($EventSource) - - if (-not [System.Diagnostics.EventLog]::Exists($TargetLog)) { - New-EventLog -LogName $TargetLog -Source $EventSource -ErrorAction Stop - } - else { - New-EventLog -LogName $TargetLog -Source $EventSource -ErrorAction Stop - } - - Write-LogHybrid "Event source '$EventSource' rebound to '$TargetLog'." Success SVSModule -LogToEvent - } - catch { - Write-LogHybrid "Failed to rebind event source '$EventSource' to log '$TargetLog': $($_.Exception.Message)" Error SVSModule -LogToEvent - } - } - - - function Start-ToolkitInstallation { - Initialize-NuGetProvider - Start-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 - - # After module install, repair Event Log binding for legacy systems - Repair-SVSMspEventLogBinding -EventSource "SVSMSP_Module" -TargetLog "SVSMSP Events" - - Write-LogHybrid "Toolkit installation completed." "Success" "SVSModule" -LogToEvent - } - - - Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent - if ($Cleanup) { - - Start-Cleanup - Remove-SVSDeploymentRegKey - return - - } - if ($InstallToolkit) { - Start-ToolkitInstallation; return - } - # default if no switch passed: - Start-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 Ensure-SVSMspEventLog { - [CmdletBinding()] - param( - [string]$EventSource = "SVSMSP_Module", - [string]$EventLog = "SVSMSP Events" - ) - - try { - # If the log already exists, we're done - if ([System.Diagnostics.EventLog]::Exists($EventLog)) { - return - } - - # If the source exists but is bound to another log, rebind it - if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) { - $current = [System.Diagnostics.EventLog]::LogNameFromSourceName($EventSource, '.') - if ($current -ne $EventLog) { - [System.Diagnostics.EventLog]::DeleteEventSource($EventSource) - } - } - - New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop - } - catch { - Write-Host "[Warning] Failed to ensure Event Log '$EventLog': $($_.Exception.Message)" -ForegroundColor Yellow - } -} - -function Write-SvsMspEventLog { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$Message, - - [ValidateSet('Information','Warning','Error')] - [string]$EntryType = 'Information', - - [int]$EventId = 1000, - - [string]$EventSource = 'SVSMSP_Module', - [string]$EventLog = 'SVSMSP Events' - ) - - # First attempt - try { - Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $EntryType -EventId $EventId -Message $Message -ErrorAction Stop - return - } - catch { - $errMsg = $_.Exception.Message - - # Only self-heal for the specific missing-log case - if ($errMsg -like '*The Log name*SVSMSP Events*does not exist*') { - Ensure-SVSMspEventLog -EventSource $EventSource -EventLog $EventLog - - # Retry once after creating the log - try { - Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $EntryType -EventId $EventId -Message $Message -ErrorAction Stop - } - catch { - Write-Host "[Warning] Failed to write to '$EventLog' even after Ensure-SVSMspEventLog: $($_.Exception.Message)" -ForegroundColor Yellow - } - } - else { - Write-Host "[Warning] Failed to write to '$EventLog': $errMsg" -ForegroundColor Yellow - } - } -} - - - 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 ScriptAutomationMonkey.) - - .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 = "SAMY", - - # 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 ($LogToEvent.IsPresent) { - Ensure-SVSMspEventLog -EventSource $EventSource -EventLog $EventLog - } - - 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 - - #region Computer rename helpers - - function Test-ComputerName { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)] - [string]$Name - ) - - if ([string]::IsNullOrWhiteSpace($Name)) { return $false } - if ($Name.Length -gt 15) { return $false } - if ($Name -notmatch '^[A-Za-z0-9-]+$') { return $false } - return $true - } - - #endregion Computer rename helpers - - #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='Invoke-setSVSPowerPlan'; Page='onboard'; Column='left' }, - @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Invoke-InstallSVSMSP'; Page='onboard'; Column='left' }, - @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Invoke-InstallCyberQP'; Page='onboard'; Column='left' }, - @{ Id='installHelpDesk'; Name='installHelpDesk'; Label='Install HelpDesk'; HandlerFn='Invoke-InstallHelpDesk'; Page='onboard'; Column='left' }, - @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Invoke-InstallThreatLocker'; Page='onboard'; Column='left' }, - @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Invoke-InstallRocketCyber'; Page='onboard'; Column='left' }, - @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Invoke-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='Invoke-SetEdgeDefaultSearchEngine';Page='onboard'; Column='right' }, - - # Off-Boarding - @{ Id='offUninstallCyberQP'; Name='offUninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Invoke-UninstallCyberQP'; Page='offboard' }, - @{ Id='offUninstallHelpDesk'; Name='offUninstallHelpDesk'; Label='Uninstall HelpDesk'; HandlerFn='Invoke-UninstallHelpDesk'; Page='offboard' }, - @{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker'; HandlerFn='Invoke-UninstallThreatLocker'; Page='offboard' }, - @{ Id='offUninstallRocketCyber'; Name='offUninstallRocketCyber'; Label='Uninstall RocketCyber'; HandlerFn='Invoke-UninstallRocketCyber'; Page='offboard' }, - @{ Id='offCleanupSVSMSPModule'; Name='offCleanupSVSMSPModule'; Label='Cleanup SVSMSP Toolkit'; HandlerFn='Invoke-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='Invoke-InstallChrome'; Page='SVSApps' }, - @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Invoke-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 Publish-Checkboxes - function Publish-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)" - - if ($_.SubOptions) { - $subHtml = ( - $_.SubOptions | - ForEach-Object { - "" - } - ) -join "`n" - - $html += @" - -"@ - } - - $html - } - ) -join "`n" - } # end function Publish-Checkboxes - - - #endregion Publish-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 - - # Friendly branch label based on $Script:SamyBranch - $branchDisplay = switch ($Script:SamyBranch.ToLower()) { - 'main' { 'Main / Stable' } - 'beta' { 'Beta' } - default { $Script:SamyBranch } - } - - if ($mod) { - return "
- Module Version: $($mod.Version)
- UI Branch: $branchDisplay -
" - } - - 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 - } - } - 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 = Publish-Checkboxes -Page 'onboard' -Column 'left' - $onboardRight = Publish-Checkboxes -Page 'onboard' -Column 'right' - $offboard = Publish-Checkboxes -Page 'offboard' -Column '' - $tweaks = Publish-Checkboxes -Page 'tweaks' -Column '' - $apps = Publish-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" - - # Human friendly branch label for UI - $branchDisplay = switch ($Script:SamyBranch.ToLower()) { - 'main' { 'Main / Stable' } - 'beta' { 'Beta' } - default { $Script:SamyBranch } - } - - # - # 3) Pull CSS/JS from Gitea and inline them - # - $cssContent = Get-RemoteText -Url $Script:SamyCssUrl - $jsContent = Get-RemoteText -Url $Script:SamyJsUrl - - # Make the CSS background-image follow $Script:SamyBgLogoUrl - if ($cssContent) { - $pattern = 'background-image:\s*url\("SAMY\.png"\);?' # matches with or without extra spaces/semicolon - $replacement = "background-image: url('$Script:SamyBgLogoUrl');" - $cssContent = [regex]::Replace($cssContent, $pattern, $replacement) - } - - - # - # 4) HTML template - **no external link/script src** anymore, all inlined - # - $htmlTemplate = @" - - - - - -Script Automation Monkey - - - - - -
- -
- SVS Logo - {{moduleVersion}} -
- - -
- Script Automation Monkey (Yeah!) -
- - -
- -
- -
- -
-
-

On-Boarding

-

This new deployment method ensures everything is successfully deployed with greater ease!

- -
-
-

SVSMSP Stack

- - {{onboardLeftColumn}} -
-
-

Optional

- - {{onboardRightColumn}} - - -
- -
- - -
-
- - - - - -
- -
-

Off-Boarding

-
-
-

Remove Stack

- - {{offboardCheckboxes}} -
-
-
- -
-

Tweaks

-
-
-

Tweaks

- {{tweaksCheckboxes}} -
-
-
- -
-

SVS APPs

-
-
-

Applications

- {{appsCheckboxes}} -
-
-
- - -
-

Devices

-

Manage printers and other client devices.

- - -
- -
- - -
-
- - - - - - -
-
-
- - - - - - - -
- - -
- - - -"@ - - # - # 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 - #region HTTP responder helpers - - function Send-Text { - param($Context, $Text) - if (-not $Context -or -not $Context.Response) { - return - } - $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 Send-HTML { - [CmdletBinding()] - param( - [Parameter(Mandatory = $true)][object] $Context, - [Parameter(Mandatory = $true)][string] $Html - ) - if (-not $Context -or -not $Context.Response) { - return - } - $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 Send-JSON { - [CmdletBinding()] - param( - $Context, - $Object - ) - - if (-not $Context -or -not $Context.Response) { - return - } - - try { - # 🔹 Normalize $Object so we never feed $null to GetBytes - if ($null -eq $Object) { - Write-LogHybrid "Send-JSON called with `$null object; returning empty JSON array." Warning Printers -LogToEvent - $json = '[]' - } - else { - # If ConvertTo-Json fails, force an empty array string instead of bubbling $null - try { - $json = $Object | ConvertTo-Json -Depth 5 -ErrorAction Stop - } - catch { - Write-LogHybrid "Send-JSON serialization failed: $($_.Exception.Message); returning empty JSON array." Error Printers -LogToEvent - $json = '[]' - } - } - - # 🔹 Final safety: ensure we always pass a *string* to GetBytes - $json = [string]$json - - $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() - } - catch { - # Last-resort error handling - don't let the whole request crash - Write-LogHybrid "Send-JSON fatal error: $($_.Exception.Message)" Error Printers -LogToEvent - try { - $fallback = '[]' - $bytes = [Text.Encoding]::UTF8.GetBytes($fallback) - $Context.Response.ContentType = 'application/json' - $Context.Response.ContentLength64 = $bytes.Length - $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) - $Context.Response.OutputStream.Close() - } - catch { - # If even this fails, just give up silently - we've already logged it. - } - } -} - - #endregion HTTP responder helpers - - function Invoke-TasksCompleted { - param($Context) - - Write-LogHybrid "All UI-selected tasks processed" Info UI -LogToEvent - Send-Text $Context "Tasks completion acknowledged." - } - - #region Datto handlers - function Invoke-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 - Send-JSON $Context $sites - } - catch { - # Log the exception and return HTTP 500 - Write-LogHybrid "Invoke-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent - $Context.Response.StatusCode = 500 - Send-Text $Context "Internal server error fetching sites." - } - } - - #endregion Datto handlers - - #region Onboarding handlers - # On-boarding handlers - function Invoke-SetSVSPowerPlan { - param($Context) - - # 1) call into your module - Set-SVSPowerPlan - - # 2) log & write back a simple text response - Write-LogHybrid "PowerPlan set" "Success" "OnBoard" - Send-Text $Context "PowerPlan applied" - } - - function Invoke-InstallSVSMSP { - param($Context) - Write-LogHybrid "HTTP trigger: Invoke-InstallSVSMSP" "Info" "OnBoard" - try { - Install-SVSMSP -InstallToolkit - Send-Text $Context "SVSMSP Module installed/updated." - } catch { - Write-LogHybrid "Error in Install-SVSMSP: $_" "Error" "OnBoard" - Send-Text $Context "ERROR: $_" - } - } - - function Invoke-InstallCyberQP { - param($Context) - - # 1) call into your module - Install-CyberQP - - # 2) log & write back a simple text response - Write-LogHybrid "CyberQP installed" "Success" "OnBoard" - Send-Text $Context "CyberQP installed" - } - - function Invoke-InstallThreatLocker { - param($Context) - - # 1) call into your module - Install-ThreatLocker - - # 2) log & write back a simple text response - Write-LogHybrid "ThreatLocker installed" "Success" "OnBoard" - Send-Text $Context "ThreatLocker installed" - } - - function Invoke-InstallRocketCyber { - param($Context) - - # 1) call into your module - Install-RocketCyber - - # 2) log & write back a simple text response - Write-LogHybrid "RocketCyber installed" "Success" "OnBoard" - Send-Text $Context "RocketCyber installed" - } - - function Invoke-InstallHelpDesk { - param($Context) - - # 1) call into your module - Install-svsHelpDesk - - # 2) log & write back a simple text response - Write-LogHybrid "SVS HelpDesk installed" "Success" "OnBoard" - Send-Text $Context "SVS HelpDesk installed" - } - - function Invoke-SetEdgeDefaultSearchEngine { - param($Context) - - try { - Write-LogHybrid "Configuring Edge default search provider" Info OnBoard - set-EdgeDefaultSearchEngine - Write-LogHybrid "Edge default search set to Google" Success OnBoard - Send-Text $Context "Edge default search provider configured." - } catch { - Write-LogHybrid "Failed to set Edge default search: $($_.Exception.Message)" Error OnBoard - Send-Text $Context "ERROR: $($_.Exception.Message)" - } - } - - function Invoke-RenameComputer { - param($Context) - - try { - if ($Context.Request.HttpMethod -ne 'POST') { - $Context.Response.StatusCode = 405 - Send-Text $Context 'Use POST' - return - } - - # Read raw JSON body - $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() - if (-not $rawBody) { - $Context.Response.StatusCode = 400 - Send-Text $Context 'Missing request body.' - return - } - - try { - $body = $rawBody | ConvertFrom-Json - } catch { - $Context.Response.StatusCode = 400 - Send-Text $Context 'Invalid JSON body.' - return - } - - $newName = $body.newName - - if (-not (Test-ComputerName -Name $newName)) { - Write-LogHybrid "RenameComputer: invalid computer name '$newName'." Error OnBoard -LogToEvent - $Context.Response.StatusCode = 400 - Send-JSON $Context @{ - Success = $false - Error = "Invalid computer name. Must be 1-15 characters and use only letters, numbers, and hyphens." - } - return - } - - Write-LogHybrid "RenameComputer: renaming computer to '$newName'." Info OnBoard -LogToEvent - - try { - Rename-Computer -NewName $newName -Force -ErrorAction Stop - } catch { - Write-LogHybrid "RenameComputer: rename failed: $($_.Exception.Message)" Error OnBoard -LogToEvent - $Context.Response.StatusCode = 500 - Send-JSON $Context @{ - Success = $false - Error = $_.Exception.Message - } - return - } - - Write-LogHybrid "RenameComputer: rename complete, reboot required for new name to apply." Success OnBoard -LogToEvent - - Send-JSON $Context @{ - Success = $true - NewName = $newName - Note = "Rename successful. A reboot is required for the new name to take effect." - } - } catch { - Write-LogHybrid "Invoke-RenameComputer fatal error: $($_.Exception.Message)" Error OnBoard -LogToEvent - $Context.Response.StatusCode = 500 - Send-Text $Context "Internal error during computer rename." - } - } - - - #endregion Onboarding handlers - - function Invoke-InstallDattoRMM { - param($Context) - - try { - if ($Context.Request.HttpMethod -ne 'POST') { - $Context.Response.StatusCode = 405 - Send-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 - Send-Text $Context "Triggered DattoRMM for $($data.Name)" - } - catch { - # Log the exception and return HTTP 500 - Write-LogHybrid "Invoke-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent - $Context.Response.StatusCode = 500 - Send-Text $Context "Internal server error during DattoRMM install." - } -} - -#endregion Datto handlers - - #region App handlers -function Invoke-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 - Send-Text $Context "Chrome installed" - } catch { - Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - -function Invoke-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 - Send-Text $Context "Acrobat Reader installed" - } catch { - Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - - #endregion App handlers - - #region Offboarding handlers - -function Invoke-UninstallCyberQP { - param($Context) - - try { - if (Get-Command Uninstall-CyberQP -ErrorAction Stop) { - Uninstall-CyberQP - Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent - Send-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 - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - -function Invoke-UninstallHelpDesk { - param($Context) - - try { - if (Get-Command Uninstall-HelpDesk -ErrorAction Stop) { - Uninstall-HelpDesk - Write-LogHybrid "SVS HelpDesk uninstalled" Success OffBoard -LogToEvent - Send-Text $Context "SVS HelpDesk uninstalled." - } else { - throw "Uninstall-HelpDesk cmdlet not found in SVSMSP toolkit." - } - } - catch { - Write-LogHybrid "Error uninstalling SVS HelpDesk: $($_.Exception.Message)" Error OffBoard -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - -function Invoke-UninstallThreatLocker { - param($Context) - - try { - if (Get-Command Uninstall-ThreatLocker -ErrorAction Stop) { - Uninstall-ThreatLocker - Write-LogHybrid "ThreatLocker uninstalled" Success OffBoard -LogToEvent - Send-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 - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - -function Invoke-UninstallRocketCyber { - param($Context) - - try { - if (Get-Command Uninstall-RocketCyber -ErrorAction Stop) { - Uninstall-RocketCyber - Write-LogHybrid "RocketCyber uninstalled" Success OffBoard -LogToEvent - Send-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 - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - -function Invoke-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 completed (module, repo, registry)." Success OffBoard -LogToEvent - Send-Text $Context "SVSMSP toolkit cleanup completed." - } else { - throw "Install-SVSMSP function not found in current session." - } - } - catch { - Write-LogHybrid "Error cleaning up SVSMSP toolkit: $($_.Exception.Message)" Error OffBoard -LogToEvent - Send-Text $Context "ERROR: $($_.Exception.Message)" - } -} - - #endregion Offboarding handlers - -#region Printer handlers - -function Get-SamyDriverRootFolder { - [CmdletBinding()] - param() - - $root = Join-Path $env:ProgramData 'SVS\Samy\Drivers' - - if (-not (Test-Path $root)) { - try { - New-Item -Path $root -ItemType Directory -Force | Out-Null - Write-LogHybrid "Created driver root folder '$root'." Info Printers -LogToEvent - } catch { - Write-LogHybrid "Failed to create driver root folder '$root': $($_.Exception.Message)" Warning Printers -LogToEvent - } - } - - return $root -} - -function Get-SamyDriverFolderForProfile { - [CmdletBinding()] - param( - [Parameter(Mandatory)][pscustomobject]$Profile - ) - - $root = Get-SamyDriverRootFolder - - # Optional override if you ever add DriverFolderName to the profile - if ($Profile.PSObject.Properties.Name -contains 'DriverFolderName' -and $Profile.DriverFolderName) { - $folderName = $Profile.DriverFolderName - } else { - $folderName = "$($Profile.ClientCode)_$($Profile.ProfileName)" - } - - $dest = Join-Path $root $folderName - - if (-not (Test-Path $dest)) { - try { - New-Item -Path $dest -ItemType Directory -Force | Out-Null - Write-LogHybrid "Created driver folder '$dest'." Info Printers -LogToEvent - } catch { - Write-LogHybrid "Failed to create driver folder '$dest': $($_.Exception.Message)" Warning Printers -LogToEvent - } - } - - return $dest -} - -function Get-SamyDriverPackageUrl { - [CmdletBinding()] - param( - [Parameter(Mandatory)][pscustomobject]$Profile - ) - - # If profile explicitly provides a full URL, prefer that - if ($Profile.PSObject.Properties.Name -contains 'DriverPackageUrl' -and $Profile.DriverPackageUrl) { - return $Profile.DriverPackageUrl - } - - # Otherwise build it from SamyRepoBase / SamyBranch and DriverPackagePath - if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) { - # Example: https://git.../SAMY/raw/branch/beta/Drivers/.../package.zip?raw=1 - return "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1" - } - - return $null -} - - -function Get-SamyClientListFromServer { - <# - .SYNOPSIS - Queries the Node.js service for a list of clients/printers using a SAMYPW header. - - .DESCRIPTION - Calls your Node.js endpoint with: - - -Method Post - -Headers @{ SAMYPW = '' } - -ContentType 'application/json' - - and returns the JSON it sends back (normalized to an array). - - .PARAMETER Uri - The HTTP/HTTPS endpoint (e.g. https://bananas.svstools.ca/getprinters). - - .PARAMETER Password - Password/API key that will be sent as the SAMYPW header. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$Uri, - - [Parameter(Mandatory=$false)] - [AllowEmptyString()] - [string]$Password = "" - ) - - try { - Write-LogHybrid "Calling client list service at $Uri" Info Printers -LogToEvent - - $headers = @{} - if (-not [string]::IsNullOrEmpty($Password)) { - $headers.SAMYPW = $Password - } - - $irmParams = @{ - Uri = $Uri - Method = 'Get' - ContentType = 'application/json' - ErrorAction = 'Stop' - } - - if ($headers.Count -gt 0) { $irmParams.Headers = $headers } - - $resp = Invoke-RestMethod @irmParams - - - if (-not $resp) { - Write-LogHybrid "Client list service returned no data." Warning Printers -LogToEvent - return @() - } - - # Normalize to an array so callers can rely on it - if ($resp -is [System.Collections.IEnumerable] -and -not ($resp -is [string])) { - return @($resp) - } else { - return ,$resp - } - } - catch { - Write-LogHybrid ("Get-SamyClientListFromServer failed for {0}: {1}" -f $Uri, $_.Exception.Message) Error Printers -LogToEvent - return @() - } -} - -function Invoke-GetPrinters { - param($Context) - - try { - if ($Context.Request.HttpMethod -ne 'POST') { - $Context.Response.StatusCode = 405 - Send-Text $Context 'Use POST' - return - } - - # Read JSON body: { "password": "..." } - $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() - if (-not $rawBody) { - $Context.Response.StatusCode = 400 - Send-Text $Context 'Missing request body.' - return - } - - try { - $body = $rawBody | ConvertFrom-Json - } catch { - $Context.Response.StatusCode = 400 - Send-Text $Context 'Invalid JSON body.' - return - } - - $password = ($body.password | ForEach-Object { "$_".Trim() }) - if ($null -eq $password) { $password = "" } # allow blank - - - - $uri = 'https://bananas.svstools.ca/getprinters' - Write-LogHybrid "Fetching printers from $uri" Info Printers -LogToEvent - - # NOTE: We never log the actual password - $printers = Get-SamyClientListFromServer -Uri $uri -Password $password - - # EXTRA SAFETY: never pass $null to Send-JSON - if ($null -eq $printers) { - Write-LogHybrid "Get-SamyClientListFromServer returned `$null; sending empty JSON array." Warning Printers -LogToEvent - $printers = @() - } - - # Always update local printers.json with latest from bananas - # but don't wipe a good file when we got *nothing* back. - try { - Update-SamyPrinterConfig -PrinterProfiles $printers -SkipIfEmpty - } - catch { - Write-LogHybrid "Update-SamyPrinterConfig failed: $($_.Exception.Message)" Warning Printers -LogToEvent - } - - # Return raw objects as JSON; JS will filter/group - Send-JSON $Context $printers - } - catch { - Write-LogHybrid "Invoke-GetPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent - $Context.Response.StatusCode = 500 - Send-Text $Context "Internal server error fetching printers." - } -} - -function Invoke-InstallPrinters { - param($Context) - - try { - if ($Context.Request.HttpMethod -ne 'POST') { - $Context.Response.StatusCode = 405 - Send-Text $Context 'Use POST' - return - } - - $rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() - if (-not $rawBody) { - $Context.Response.StatusCode = 400 - Send-Text $Context 'Missing request body.' - return - } - - try { - $body = $rawBody | ConvertFrom-Json - } catch { - $Context.Response.StatusCode = 400 - Send-Text $Context 'Invalid JSON body.' - return - } - - $printers = $body.printers - if (-not $printers -or $printers.Count -eq 0) { - $Context.Response.StatusCode = 400 - Send-Text $Context 'No printers specified.' - return - } - - Write-LogHybrid "Received request to install $($printers.Count) printers." Info Printers -LogToEvent - - $successCount = 0 - $failures = @() - - foreach ($p in $printers) { - # Expecting fields from JSON: - # ClientCode = 'ABC' - # ProfileName = 'FrontDesk' - # SetAsDefault = $true/$false (optional) - - $clientCode = $p.ClientCode - $profileName = $p.ProfileName - $setDefault = $false - - if ($p.PSObject.Properties.Name -contains 'SetAsDefault' -and $p.SetAsDefault) { - $setDefault = $true - } - - if (-not $clientCode -or -not $profileName) { - $msg = "Skipping printer entry because ClientCode or ProfileName is missing." - Write-LogHybrid $msg Warning Printers -LogToEvent - $failures += $msg - continue - } - - $summary = "ClientCode=$clientCode ProfileName=$profileName DisplayName=$($p.DisplayName) Location=$($p.Location) SetAsDefault=$setDefault" - Write-LogHybrid "Installing printer ($summary)" Info Printers -LogToEvent - - try { - # SAFE PHASE: we call with -WhatIf so no real change happens - Invoke-SamyPrinterInstall ` - -ClientCode $clientCode ` - -ProfileName $profileName ` - -SetAsDefault:$setDefault ` - #-WhatIf - - $successCount++ - } - catch { - $errMsg = "Failed to install printer ($summary): $($_.Exception.Message)" - Write-LogHybrid $errMsg Error Printers -LogToEvent - $failures += $errMsg - } - } - - $result = @{ - SuccessCount = $successCount - FailureCount = $failures.Count - Failures = $failures - Message = "Printer install (WHATIF) processed. Check SAMY logs for detail." - } - - Send-JSON $Context $result - } - catch { - Write-LogHybrid "Invoke-InstallPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent - $Context.Response.StatusCode = 500 - Send-Text $Context "Internal server error installing printers." - } -} - -#region Printer core (local config + install) - -function Get-SamyPrinterLocalConfigPath { - [CmdletBinding()] - param() - - # Use a stable location on every machine - $configDir = Join-Path $env:ProgramData 'SVS\Samy\Printers' - - if (-not (Test-Path $configDir)) { - try { - New-Item -Path $configDir -ItemType Directory -Force | Out-Null - Write-LogHybrid "Created printer config folder at '$configDir'." Info Printers -LogToEvent - } - catch { - Write-LogHybrid "Failed to create printer config folder '$configDir': $($_.Exception.Message)" Warning Printers -LogToEvent - } - } - - return (Join-Path $configDir 'printers.json') -} - -function Get-SamyPrinterConfigFromFile { - [CmdletBinding()] - param() - - $path = Get-SamyPrinterLocalConfigPath - - if (-not (Test-Path $path)) { - throw "Local printer config file not found at '$path'. Create or update printers.json first." - } - - $json = Get-Content -Path $path -Raw -ErrorAction Stop - $profiles = $json | ConvertFrom-Json - - if (-not $profiles) { - throw "Printer config file '$path' is empty or invalid JSON." - } - - return $profiles -} - -# Per-session cache -$Script:Samy_PrinterProfiles = $null - -function Get-SamyPrinterProfiles { - <# - .SYNOPSIS - Returns all printer profiles, optionally filtered by ClientCode. - #> - [CmdletBinding()] - param( - [string]$ClientCode - ) - - if (-not $Script:Samy_PrinterProfiles) { - $Script:Samy_PrinterProfiles = Get-SamyPrinterConfigFromFile - } - - $result = $Script:Samy_PrinterProfiles - - if ($PSBoundParameters.ContainsKey('ClientCode') -and $ClientCode) { - $result = $result | Where-Object { $_.ClientCode -eq $ClientCode } - } - - return $result -} - -function Get-SamyPrinterProfile { - <# - .SYNOPSIS - Returns a single printer profile for a given ClientCode and ProfileName. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)][string]$ClientCode, - [Parameter(Mandatory)][string]$ProfileName - ) - - $profiles = Get-SamyPrinterProfiles -ClientCode $ClientCode - $match = $profiles | Where-Object { $_.ProfileName -eq $ProfileName } - - if (-not $match) { - throw "No printer profile found for ClientCode '$ClientCode' and ProfileName '$ProfileName'." - } - - if ($match.Count -gt 1) { - throw "Multiple printer profiles found for ClientCode '$ClientCode' and ProfileName '$ProfileName'. De-duplicate in printers.json." - } - - return $match -} - -function Ensure-SamyPrinterDriver { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [pscustomobject]$Profile - ) - - $driverName = $Profile.DriverName - if (-not $driverName) { - throw "Profile '$($Profile.ProfileName)' has no DriverName defined in printer config." - } - - # Already installed? - $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue - if ($existingDriver) { - Write-LogHybrid "Printer driver '$driverName' already installed." Info Printers -LogToEvent - return - } - - Write-LogHybrid "Printer driver '$driverName' not found. Preparing to install." Info Printers -LogToEvent - - # ----------------------------- - # 0) Decide where driver files live locally (per-profile) - # ----------------------------- - $localDriverRoot = Get-SamyDriverFolderForProfile -Profile $Profile - - - # ----------------------------- - # 1) Start with any static local INF path, if defined - # ----------------------------- - $infPath = $null - if ($Profile.PSObject.Properties.Name -contains 'DriverInfPath' -and $Profile.DriverInfPath) { - if (Test-Path $Profile.DriverInfPath) { - $infPath = $Profile.DriverInfPath - Write-LogHybrid "Using existing INF path '$infPath' for driver '$driverName'." Info Printers -LogToEvent - } else { - Write-LogHybrid "Configured DriverInfPath '$($Profile.DriverInfPath)' does not exist, will try repo download." Warning Printers -LogToEvent - } - } - - # ----------------------------- - # 2) Try downloading a driver package from repo (404 is *not* fatal) - # ----------------------------- - $packageDownloaded = $false - - if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) { - $driverPackageUrl = "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1" - $localZip = Join-Path $localDriverRoot "package.zip" - - Write-LogHybrid "Attempting to download driver package from $driverPackageUrl." Info Printers -LogToEvent - - try { - Invoke-WebRequest -Uri $driverPackageUrl -OutFile $localZip -UseBasicParsing -ErrorAction Stop - Write-LogHybrid "Downloaded driver package from $driverPackageUrl to $localZip." Success Printers -LogToEvent - $packageDownloaded = $true - } - catch [System.Net.WebException] { - $response = $_.Exception.Response - $statusCode = $null - if ($response -and $response.StatusCode) { - $statusCode = [int]$response.StatusCode - } - - if ($statusCode -eq 404) { - # ★ This is the new behavior: warn, but do NOT throw. - Write-LogHybrid "Driver package not found at $driverPackageUrl (404). Falling back to INF-only install for '$($Profile.DisplayName)'." Warning Printers -LogToEvent - # We just continue - maybe a local INF exists or will exist. - } - else { - Write-LogHybrid "Driver package download failed ($statusCode) from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent - throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)" - } - } - catch { - Write-LogHybrid "Driver package download failed from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent - throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)" - } - } - else { - Write-LogHybrid "No DriverPackagePath defined for '$($Profile.DisplayName)'; will rely on local INF." Info Printers -LogToEvent - } - - # ----------------------------- - # 2b) If we *did* download a package, expand it and try to locate the INF - # ----------------------------- - if ($packageDownloaded) { - try { - Expand-Archive -Path $localZip -DestinationPath $localDriverRoot -Force - Write-LogHybrid "Expanded driver package to '$localDriverRoot'." Info Printers -LogToEvent - } - catch { - Write-LogHybrid "Failed to expand driver package '$localZip': $($_.Exception.Message)" Error Printers -LogToEvent - throw "Failed to expand driver package '$localZip': $($_.Exception.Message)" - } - - # If we don't yet have an INF path, try to derive it from DriverInfName - # If we don't yet have an INF path, try to derive it from DriverInfName - if (-not $infPath) { - if ($Profile.PSObject.Properties.Name -contains 'DriverInfName' -and $Profile.DriverInfName) { - - # 1) First try: directly under the destination root - $candidateInf = Join-Path $localDriverRoot $Profile.DriverInfName - if (Test-Path $candidateInf) { - $infPath = $candidateInf - Write-LogHybrid "Resolved INF from package as '$infPath' using DriverInfName '$($Profile.DriverInfName)' at root." Info Printers -LogToEvent - } - else { - Write-LogHybrid "Expected INF '$candidateInf' (from DriverInfName) not found at root; searching recursively..." Warning Printers -LogToEvent - - # 2) Second try: search subfolders for that INF name - $found = Get-ChildItem -Path $localDriverRoot -Recurse -Filter $Profile.DriverInfName -File -ErrorAction SilentlyContinue | - Select-Object -First 1 - - if ($found) { - $infPath = $found.FullName - Write-LogHybrid "Resolved INF from package as '$infPath' (found by recursive search for '$($Profile.DriverInfName)')." Info Printers -LogToEvent - } - else { - Write-LogHybrid "Could not find any '$($Profile.DriverInfName)' under '$localDriverRoot' after expanding package." Error Printers -LogToEvent - } - } - } - else { - Write-LogHybrid "DriverInfName not defined for profile '$($Profile.ProfileName)'; cannot auto-resolve INF from expanded package." Warning Printers -LogToEvent - } - } - - } - - - - # ----------------------------- - # 4) Still nothing? Hard fail with a clear message - # ----------------------------- - if (-not $infPath -or -not (Test-Path $infPath)) { - throw "Driver '$driverName' is not installed and no valid DriverInfPath or usable driver package is available for profile '$($Profile.ProfileName)'." - } - - Write-LogHybrid "Installing printer driver '$driverName' from '$infPath'." Info Printers -LogToEvent - - # 4a) Stage the driver package with pnputil - $pnputilCmd = "pnputil.exe /add-driver `"$infPath`" /install" - Write-LogHybrid "Running: $pnputilCmd" Info Printers -LogToEvent - - $pnputilOutput = & pnputil.exe /add-driver "$infPath" /install 2>&1 - $exitCode = $LASTEXITCODE - - Write-LogHybrid "pnputil exit code: $exitCode. Output:`n$pnputilOutput" Info Printers -LogToEvent - - if ($exitCode -ne 0) { - throw "pnputil failed with exit code $exitCode installing '$driverName' from '$infPath'." - } - - # 4b) Register the printer driver with Add-PrinterDriver - try { - Write-LogHybrid "Calling Add-PrinterDriver -Name '$driverName' -InfPath '$infPath'." Info Printers -LogToEvent - Add-PrinterDriver -Name $driverName -ErrorAction Stop - } - catch { - Write-LogHybrid "Add-PrinterDriver failed for '$driverName' using '$infPath': $($_.Exception.Message)" Error Printers -LogToEvent - throw "Add-PrinterDriver failed for '$driverName': $($_.Exception.Message)" - } - - # 4c) Final verification - Start-Sleep -Seconds 2 - - $existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue - - if (-not $existingDriver) { - $sharpNames = (Get-PrinterDriver -ErrorAction SilentlyContinue | - Where-Object Name -like 'SHARP*' | - Select-Object -ExpandProperty Name) -join ', ' - - if (-not $sharpNames) { $sharpNames = '(none)' } - - Write-LogHybrid "After pnputil/Add-PrinterDriver, driver '$driverName' not found. Existing SHARP drivers: $sharpNames" Warning Printers -LogToEvent - throw "Failed to find printer driver '$driverName' after Add-PrinterDriver." - } - - Write-LogHybrid "Printer driver '$driverName' installed and detected successfully." Success Printers -LogToEvent -} - - - - - -function Install-SamyTcpIpPrinter { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [pscustomobject]$Profile, - - [switch]$SetAsDefault - ) - - $portName = $Profile.Address - $printerName = $Profile.DisplayName - - if (-not $portName) { - throw "TCP/IP printer profile '$($Profile.ProfileName)' is missing Address in printer config." - } - - if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) { - Write-Verbose "Creating TCP/IP port '$portName'." - Add-PrinterPort -Name $portName -PrinterHostAddress $Profile.Address - } - else { - Write-Verbose "TCP/IP port '$portName' already exists." - } - - $existingPrinter = Get-Printer -Name $printerName -ErrorAction SilentlyContinue - if ($existingPrinter) { - Write-Verbose "Printer '$printerName' already exists. Skipping creation." - } - else { - Write-Verbose "Creating printer '$printerName' on port '$portName' using driver '$($Profile.DriverName)'." - Add-Printer -Name $printerName -PortName $portName -DriverName $Profile.DriverName - } - - if ($SetAsDefault -or $Profile.IsDefault) { - Write-Verbose "Setting '$printerName' as default printer." - (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName) - } -} - -function Install-SamySharedPrinter { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [pscustomobject]$Profile, - - [switch]$SetAsDefault - ) - - if (-not $Profile.PrintServer -or -not $Profile.ShareName) { - throw "Shared printer profile '$($Profile.ProfileName)' is missing PrintServer or ShareName in printer config." - } - - $connectionName = "\\$($Profile.PrintServer)\$($Profile.ShareName)" - - $existing = Get-Printer -ErrorAction SilentlyContinue | - Where-Object { - $_.Name -eq $Profile.DisplayName -or - $_.ShareName -eq $Profile.ShareName - } - - if ($existing) { - Write-Verbose "Shared printer '$($Profile.DisplayName)' already connected as '$($existing.Name)'." - $printerName = $existing.Name - } - else { - Write-Verbose "Adding shared printer connection '$connectionName'." - Add-Printer -ConnectionName $connectionName - - $printerName = (Get-Printer | - Where-Object { $_.Name -like "*$($Profile.ShareName)*" } | - Select-Object -First 1 - ).Name - } - - if ($SetAsDefault -or $Profile.IsDefault) { - Write-Verbose "Setting '$printerName' as default printer." - (New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName) - } -} - -function Invoke-SamyPrinterInstall { - <# - .SYNOPSIS - Installs a printer based on a JSON-defined profile (supports -WhatIf). - #> - [CmdletBinding(SupportsShouldProcess = $true)] - param( - [Parameter(Mandatory)] - [string]$ClientCode, - - [Parameter(Mandatory)] - [string]$ProfileName, - - [switch]$SetAsDefault - ) - - try { - $profile = Get-SamyPrinterProfile -ClientCode $ClientCode -ProfileName $ProfileName - $targetName = $profile.DisplayName - - if ($PSCmdlet.ShouldProcess($targetName, "Install printer")) { - - Write-LogHybrid "Installing printer profile '$($profile.ProfileName)' for client '$ClientCode' (WhatIf=$WhatIfPreference)." Info Printers -LogToEvent - - Ensure-SamyPrinterDriver -Profile $profile - - switch ($profile.Type) { - 'TcpIp' { - Install-SamyTcpIpPrinter -Profile $profile -SetAsDefault:$SetAsDefault - } - 'Shared' { - Install-SamySharedPrinter -Profile $profile -SetAsDefault:$SetAsDefault - } - default { - throw "Unsupported printer Type '$($profile.Type)' in profile '$($profile.ProfileName)'. Use 'TcpIp' or 'Shared'." - } - } - - Write-LogHybrid "Installed printer '$($profile.DisplayName)' (Client=$ClientCode Profile=$ProfileName)." Info Printers -LogToEvent - } - } - catch { - Write-LogHybrid ( - "Printer install failed for Client={0} Profile={1}: {2}" -f $ClientCode, $ProfileName, $_.Exception.Message - ) Error Printers -LogToEvent - throw - } -} - -function Update-SamyPrinterConfig { - <# - .SYNOPSIS - Writes the fetched printer profiles to the local printers.json file. - - .DESCRIPTION - - Uses Get-SamyPrinterLocalConfigPath to determine where printers.json lives. - - Always overwrites printers.json when non-empty data is provided. - - If called with -SkipIfEmpty and the data is empty/null, it does *nothing* - so we don't wipe a good config on a bad day. - - Resets the in-memory cache so future Get-SamyPrinterProfiles calls reload from disk. - #> - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [object]$PrinterProfiles, - - [switch]$SkipIfEmpty - ) - - $path = Get-SamyPrinterLocalConfigPath - - # Normalize to array - $profilesArray = @($PrinterProfiles) - - if ($SkipIfEmpty -and ($null -eq $PrinterProfiles -or $profilesArray.Count -eq 0)) { - Write-LogHybrid "Update-SamyPrinterConfig: no printer profiles returned; keeping existing printers.json." Warning Printers -LogToEvent - return - } - - if ($profilesArray.Count -eq 0) { - Write-LogHybrid "Update-SamyPrinterConfig: zero profiles; writing empty printers.json." Warning Printers -LogToEvent - } - - try { - $profilesArray | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8 - Write-LogHybrid "Saved $($profilesArray.Count) printer profiles to '$path'." Success Printers -LogToEvent - - # Invalidate per-session cache so future reads use the new file - $Script:Samy_PrinterProfiles = $null - } - catch { - Write-LogHybrid "Failed to write printers.json to '$path': $($_.Exception.Message)" Error Printers -LogToEvent - } -} - - -#endregion Printer core (local config + install) - -#endregion Printer handlers - - - #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) { - try { - $whHeaders = @{} - if (-not [string]::IsNullOrEmpty($WebhookPassword)) { - $whHeaders.SAMYPW = $WebhookPassword - } - - $irmParams = @{ - Uri = $WebhookUrl - Method = 'Get' - ErrorAction = 'Stop' - ContentType = 'application/json' - } - - if ($whHeaders.Count -gt 0) { $irmParams.Headers = $whHeaders } - - $resp = Invoke-RestMethod @irmParams - - - $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 { - $irmParams = @{ - Uri = "$ApiUrl/api/v2/account/sites" - Method = 'Get' - ErrorAction = 'Stop' - ContentType = 'application/json' - } - - if ($headers.Count -gt 0) { $irmParams.Headers = $headers } - - $sitesResp = Invoke-RestMethod @irmParams - - $siteList = $sitesResp.sites | Sort-Object name | 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 - Send-Text $Context "Server shutting down." - # stop the listener loop - $Global:Listener.Stop() - return - } - - # ---- Tasks completed notification ---- - if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'tasksCompleted') { - Invoke-TasksCompleted $Context - return - } - - # ---- Fetch Sites endpoint ---- - if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') { - Invoke-FetchSites $Context - return - } - - # ---- Rename Computer endpoint ---- - if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'renameComputer') { - Invoke-RenameComputer $Context - return - } - - # ---- Printer endpoints ---- - if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getprinters') { - Invoke-GetPrinters $Context - return - } - - if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'installprinters') { - Invoke-InstallPrinters $Context - return - } - - # ---- Serve UI pages ---- - if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) { - $page = if ($path -eq '') { 'onboard' } else { $path } - $html = Get-UIHtml -Page $page - Send-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 - Send-Text $Context '404 - Not Found' - } - #endregion Dispatch-Request - - #region EntryPoint: Define Invoke-ScriptAutomationMonkey - - # ───────────────────────────────────────────────────────────────────────── - # 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 - - } - - - - 'Offboard' { - Write-LogHybrid "Headless offboarding requested" Info OffBoard -LogToEvent - $offboardTasks = $Global:SamyTasks | Where-Object Page -EQ 'offboard' - if (-not $offboardTasks) { - Write-LogHybrid "No offboard tasks configured" Warning OffBoard -LogToEvent - return - } - - if (-not $PSCmdlet.ShouldProcess("Full off-boarding flow", "Execute every offboard task")) { - return - } - - foreach ($task in $offboardTasks) { - try { - Write-LogHybrid "Running offboard task: $($task.Label)" Info OffBoard -LogToEvent - if (-not (Get-Command $task.HandlerFn -ErrorAction SilentlyContinue)) { - Write-LogHybrid "Missing handler $($task.HandlerFn)" Error OffBoard -LogToEvent - continue - } - & $task.HandlerFn $null - } catch { - Write-LogHybrid "Offboard task $($task.Label) failed: $($_.Exception.Message)" Error OffBoard -LogToEvent - } - } - - Write-LogHybrid "Headless offboarding completed" Success OffBoard -LogToEvent - return - } - - - 'UI' { - $url = "http://localhost:$Port/" - Write-LogHybrid "Starting ScriptAutomationMonkey 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 'OpenScriptAutomationMonkeyUI' -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-ScriptAutomationMonkey - - - } - -if ($MyInvocation.InvocationName -eq '.') { - return # dot-sourced, just define the function -} - -if ($PSBoundParameters.Count -gt 0) { - # Called like: & scriptblock -SilentInstall - Invoke-ScriptAutomationMonkey @PSBoundParameters -} -elseif ($args.Count -gt 0) { - # Old-style args: script.ps1 -SilentInstall MyOtherParam value - $namedArgs = @{} - for ($i = 0; $i -lt $args.Count; $i++) { - if ($args[$i] -is [string] -and $args[$i].StartsWith('-')) { - $key = $args[$i].TrimStart('-') - $next = if ($i + 1 -lt $args.Count) { $args[$i + 1] } else { $null } - - if ($next -and ($next -notlike '-*')) { - $namedArgs[$key] = $next - $i++ - } else { - $namedArgs[$key] = $true - } - } - } - - Invoke-ScriptAutomationMonkey @namedArgs -} -else { - # No params: default to UI - Invoke-ScriptAutomationMonkey -} - -