#region changes to be done #endregion changes to be done ## Last changes made should fix the issues we had wen running thi in Windows 11 25H2 <# .SYNOPSIS Script Automation Monkey - MSP client onboarding/offboarding toolkit with a user interface, and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment. .DESCRIPTION Install-DattoRMM is a single, unified toolkit for Datto RMM operations. It can be used interactively or via HTTP endpoints, and includes built-in validation and error trapping. Key features: - Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook. - OAuth management - automatically acquires and refreshes bearer tokens over TLS. - Site list fetching - returns the list of RMM sites; validates OutputFile to .csv or .json. - Site list saving - writes fetched site list to the user's Desktop as CSV or JSON. - Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment. - Agent download & install - downloads the Datto RMM agent installer and launches it. - Installer archiving - saves a copy of the downloaded installer to C:\Temp. - HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch to log errors and return proper HTTP 500 responses on failure. - Idempotent & WhatIf support - uses ShouldProcess for safe, testable agent installs. Throughout, secrets are never written to logs or console, and all operations produce clear success/failure messages via Write-LogHybrid. .PARAMETER UseWebhook Switch that forces credential retrieval from the webhook at WebhookUrl using WebhookPassword. When omitted, you must supply ApiUrl, ApiKey, and ApiSecretKey directly. .PARAMETER WebhookPassword Password to authenticate to the credentials-fetch webhook. Mandatory when -UseWebhook is set. .PARAMETER WebhookUrl URL of the credentials webhook endpoint. Defaults to $Global:DattoWebhookUrl. .PARAMETER ApiUrl Direct Datto RMM API base URL (used if not fetching from webhook). .PARAMETER ApiKey Direct Datto RMM API key (used if not fetching from webhook). .PARAMETER ApiSecretKey Direct Datto RMM secret (used if not fetching from webhook). .PARAMETER FetchSites Switch to fetch the list of RMM sites and skip all install or variable-push actions. .PARAMETER SaveSitesList Switch to save the fetched site list to the desktop as a file named by OutputFile. Must be used together with -FetchSites. .PARAMETER OutputFile Name of the file to write the site list to (must end in “.csv” or “.json”). Defaults to 'datto_sites.csv'. .PARAMETER PushSiteVars Switch to fetch site-specific variables and write them under HKLM:\Software\SVS\Deployment. .PARAMETER InstallRMM Switch to download and launch the Datto RMM agent installer for the specified site. .PARAMETER SaveCopy Switch to save a copy of the downloaded Datto RMM installer into C:\Temp. .PARAMETER 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://sm.svstools.com' -UseBasicParsing).Content )) ` -UseWebhook -WebhookPassword 'pwd' -SiteUID 'site-123' -SiteName 'Acme Corp' -PushSiteVars -InstallRMM # Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry. .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -ApiUrl 'https://api.example.com' ` -ApiKey 'YourApiKey' ` -ApiSecretKey 'YourSecretKey' ` -SiteUID 'site-123' ` -SiteName 'Acme Corp' ` -PushSiteVars ` -InstallRMM # Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry. .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -UseWebhook ` -WebhookPassword 'pwd' ` -FetchSites ` -SaveSitesList ` -OutputFile 'sites.json' # Fetches the full site list via webhook and saves it as JSON to your Desktop. .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -ApiUrl 'https://api.example.com' ` -ApiKey 'YourApiKey' ` -ApiSecretKey 'YourSecretKey' ` -SiteUID 'site-123' ` -SiteName 'Acme Corp' ` -SaveCopy # Downloads the RMM installer for “Acme Corp” and saves a copy under C:\Temp without running it. .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -ApiUrl 'https://api.example.com' ` -ApiKey 'YourApiKey' ` -ApiSecretKey 'YourSecretKey' ` -SiteUID 'site-123' ` -SiteName 'Acme Corp' ` -InstallRMM ` -WhatIf # Shows what would happen when installing the RMM agent, without making any changes. .EXAMPLE & ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall .EXAMPLE & ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup .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 ─── if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or (Get-ExecutionPolicy) -eq 'Restricted') { Write-Host "[Info] Relaunching with ExecutionPolicy Bypass..." -ForegroundColor Yellow if ($PSCommandPath) { powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`"" } else { powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }" } exit } # ─── TLS and silent install defaults ─── [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' #endregion Safely bypass Restricted Execution Policy function Invoke-ScriptAutomationMonkey { # ───────────────────────────────────────────────────────────────────────── # PARAMETERS + GLOBAL VARIABLES # ───────────────────────────────────────────────────────────────────────── [CmdletBinding( DefaultParameterSetName='UI', SupportsShouldProcess=$true, ConfirmImpact= 'Medium' )] param( # ───────────────────────────────────────────────────────── # Toolkit-only mode [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, # ───────────────────────────────────────────────────────── # remove Toolkit [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, # 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')] [String]$WebhookPassword, [string]$WebhookUrl = $Global:DattoWebhookUrl, # ─── only DattoFetch uses these ──────────────────────────────────── [Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites, [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList, [Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv', # ─── only DattoInstall uses these ───────────────────────────────── [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID, [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName, [Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars, [Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM, [Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy ) #region global variables # Listening port for HTTP UI $Port = 8082 # Configurable endpoints $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' # Initialize a global in-memory log cache if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { $Global:LogCache = [System.Collections.ArrayList]::new() } #endregion global variables #region SVS Module function Initialize-NuGetProvider { [CmdletBinding()] param() #region — guarantee NuGet provider is present without prompting # ─── Silent defaults ─── [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' # ─── Pre-create folder if running as SYSTEM (avoids NuGet install bug) ─── $provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies" if (-not (Test-Path $provPath)) { try { New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warning Bootstrap -LogToEvent } } # ─── Ensure PowerShellGet is available ─── if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) { try { Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent } } # ─── Ensure PackageManagement is up-to-date ─── $pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version if ($pkgMgmtVersion -lt [Version]"1.3.1") { try { Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent } } # ─── Import modules silently ─── Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null # ─── Trust PSGallery if not already ─── $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') { try { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warning Bootstrap -LogToEvent } } # ─── Ensure NuGet is installed silently ─── $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue if (-not $nuget) { try { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent } } else { Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent } # ─── Final import check ─── try { Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null } catch { Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent } #endregion — guarantee NuGet provider is present without prompting } function Install-SVSMSP { param ( [switch] $Cleanup, [switch] $InstallToolkit, [Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }), [Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }), [Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP", [Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo", [Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/" ) function 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 } } } 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 complete." "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 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 ($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 building the Menus # Define every task once here: # Id → checkbox HTML `id` # Name → URL path (`/Name`) # Label → user-visible text # HandlerFn → the PowerShell function to invoke # Page → which tab/page it appears on $Global:SamyTasks = @( # On-Boarding, left column @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' }, @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' }, @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' }, @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' }, @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' }, @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' }, @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left'; SubOptions= @( @{ Value='inputVar'; Label='Copy Site Variables' }, @{ Value='rmm'; Label='Install RMM Agent' }, @{ Value='exe'; Label='Download Executable' } ) }, # On-Boarding, right column (optional bits) @{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' }, @{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' }, # Off-Boarding @{ Id='offUninstallCyberQP'; Name='offUninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Handle-UninstallCyberQP'; Page='offboard' }, @{ Id='offUninstallSVSHelpDesk'; Name='offUninstallSVSHelpDesk'; Label='Uninstall SVS HelpDesk'; HandlerFn='Handle-UninstallSVSHelpDesk'; Page='offboard' }, @{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker'; HandlerFn='Handle-UninstallThreatLocker'; Page='offboard' }, @{ Id='offUninstallRocketCyber'; Name='offUninstallRocketCyber'; Label='Uninstall RocketCyber'; HandlerFn='Handle-UninstallRocketCyber'; Page='offboard' }, @{ Id='offCleanupSVSMSPModule'; Name='offCleanupSVSMSPModule'; Label='Cleanup SVSMSP Toolkit'; HandlerFn='Handle-CleanupSVSMSP'; Page='offboard' }, # Tweaks @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' }, # SVS Apps @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }, @{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' }, @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' } ) Write-LogHybrid "Tasks by page: onboard=$( ($Global:SamyTasks | Where-Object Page -eq 'onboard').Count ) offboard=$( ($Global:SamyTasks | Where-Object Page -eq 'offboard').Count ) tweaks=$( ($Global:SamyTasks | Where-Object Page -eq 'tweaks').Count ) apps=$( ($Global:SamyTasks | Where-Object Page -eq 'SVSApps').Count )" Info UI -LogToEvent #endregion building the Menus #region 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 = "" 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 if ($mod) { return "