diff --git a/samy.ps1 b/samy.ps1 new file mode 100644 index 0000000..8b99a3a --- /dev/null +++ b/samy.ps1 @@ -0,0 +1,2549 @@ +#region changes to be done + +#endregion changes to be done + +## Last changes made should fix the issues we had wen running thi in Windows 11 25H2 + +<# +.SYNOPSIS + ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface, + and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment. + +.DESCRIPTION + Install-DattoRMM is a single, unified toolkit for Datto RMM operations. It can be used + interactively or via HTTP endpoints, and includes built-in validation and error trapping. + + Key features: + - Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook. + - OAuth management - automatically acquires and refreshes bearer tokens over TLS. + - Site list fetching - returns the list of RMM sites; validates OutputFile to .csv or .json. + - Site list saving - writes fetched site list to the user's Desktop as CSV or JSON. + - Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment. + - Agent download & install - downloads the Datto RMM agent installer and launches it. + - Installer archiving - saves a copy of the downloaded installer to C:\Temp. + - HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch + to log errors and return proper HTTP 500 responses on failure. + - Idempotent & WhatIf support - uses ShouldProcess for safe, testable agent installs. + + Throughout, secrets are never written to logs or console, and all operations produce + clear success/failure messages via Write-LogHybrid. + + +.PARAMETER UseWebhook + Switch that forces credential retrieval from the webhook at WebhookUrl using WebhookPassword. + When omitted, you must supply ApiUrl, ApiKey, and ApiSecretKey directly. + +.PARAMETER WebhookPassword + Password to authenticate to the credentials-fetch webhook. Mandatory when -UseWebhook is set. + +.PARAMETER WebhookUrl + URL of the credentials webhook endpoint. Defaults to $Global:DattoWebhookUrl. + +.PARAMETER ApiUrl + Direct Datto RMM API base URL (used if not fetching from webhook). + +.PARAMETER ApiKey + Direct Datto RMM API key (used if not fetching from webhook). + +.PARAMETER ApiSecretKey + Direct Datto RMM secret (used if not fetching from webhook). + +.PARAMETER FetchSites + Switch to fetch the list of RMM sites and skip all install or variable-push actions. + +.PARAMETER SaveSitesList + Switch to save the fetched site list to the desktop as a file named by OutputFile. + Must be used together with -FetchSites. + +.PARAMETER OutputFile + Name of the file to write the site list to (must end in “.csv” or “.json”). + Defaults to 'datto_sites.csv'. + +.PARAMETER PushSiteVars + Switch to fetch site-specific variables and write them under HKLM:\Software\SVS\Deployment. + +.PARAMETER InstallRMM + Switch to download and launch the Datto RMM agent installer for the specified site. + +.PARAMETER SaveCopy + Switch to save a copy of the downloaded Datto RMM installer into C:\Temp. + +.PARAMETER SiteUID + The unique identifier of the Datto RMM site. Mandatory when performing install or variable-push. + +.PARAMETER SiteName + The friendly name of the Datto RMM site (used in logging). Mandatory when performing install or variable-push. + +.EXAMPLE + + & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` + -UseWebhook + -WebhookPassword 'pwd' + -SiteUID 'site-123' + -SiteName 'Acme Corp' + -PushSiteVars + -InstallRMM + + # Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry. + +.EXAMPLE + & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` + -ApiUrl 'https://api.example.com' ` + -ApiKey 'YourApiKey' ` + -ApiSecretKey 'YourSecretKey' ` + -SiteUID 'site-123' ` + -SiteName 'Acme Corp' ` + -PushSiteVars ` + -InstallRMM + + # Headlessly installs the Datto RMM agent on “Acme Corp” and writes site variables to the registry. + +.EXAMPLE + & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` + -UseWebhook ` + -WebhookPassword 'pwd' ` + -FetchSites ` + -SaveSitesList ` + -OutputFile 'sites.json' + + # Fetches the full site list via webhook and saves it as JSON to your Desktop. + +.EXAMPLE + & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` + -ApiUrl 'https://api.example.com' ` + -ApiKey 'YourApiKey' ` + -ApiSecretKey 'YourSecretKey' ` + -SiteUID 'site-123' ` + -SiteName 'Acme Corp' ` + -SaveCopy + + # Downloads the RMM installer for “Acme Corp” and saves a copy under C:\Temp without running it. + +.EXAMPLE + & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` + -ApiUrl 'https://api.example.com' ` + -ApiKey 'YourApiKey' ` + -ApiSecretKey 'YourSecretKey' ` + -SiteUID 'site-123' ` + -SiteName 'Acme Corp' ` + -InstallRMM ` + -WhatIf + + # Shows what would happen when installing the RMM agent, without making any changes. + +.EXAMPLE + & ([ScriptBlock]::Create((iwr 'sm.svstools.ca').Content )) -SilentInstall + +.EXAMPLE + & ([ScriptBlock]::Create((iwr 'sm.svstools.com').Content)) -Cleanup + +#> +#region Safely bypass Restricted Execution Policy +# ─── Safely bypass Restricted Execution Policy ─── +if ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or + (Get-ExecutionPolicy) -eq 'Restricted') { + + Write-Host "[Info] Relaunching with ExecutionPolicy Bypass..." -ForegroundColor Yellow + + if ($PSCommandPath) { + powershell.exe -NoProfile -ExecutionPolicy Bypass -File "`"$PSCommandPath`"" + } else { + powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }" + } + + exit +} + +# ─── TLS and silent install defaults ─── +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ProgressPreference = 'SilentlyContinue' +$ConfirmPreference = 'None' +#endregion Safely bypass Restricted Execution Policy + + function Invoke-ScriptMonkey { + + # ───────────────────────────────────────────────────────────────────────── + # PARAMETERS + GLOBAL VARIABLES + # ───────────────────────────────────────────────────────────────────────── + + [CmdletBinding( + DefaultParameterSetName='UI', + SupportsShouldProcess=$true, + ConfirmImpact= 'Medium' + )] + param( + # ───────────────────────────────────────────────────────── + # Toolkit-only mode + [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, + + # ───────────────────────────────────────────────────────── + # remove Toolkit + [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, + + # ───────────────────────────────────────────────────────── + # Datto headless mode + + # ─── DattoFetch & DattoInstall share the webhook creds ───────────── + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [switch]$UseWebhook, + + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [string]$WebhookPassword, + + [string]$WebhookUrl = $Global:DattoWebhookUrl, + + # ─── only DattoFetch uses these ──────────────────────────────────── + [Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites, + [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList, + [Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv', + + # ─── only DattoInstall uses these ───────────────────────────────── + [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID, + [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName, + [Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars, + [Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM, + [Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy + ) + + #region global variables + + # Listening port for HTTP UI + $Port = 8082 + + # Configurable endpoints + $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' + + # Initialize a global in-memory log cache + + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + + #endregion global variables + + #region SVS Module + + function Initialize-NuGetProvider { + [CmdletBinding()] + param() + + #region — guarantee NuGet provider is present without prompting + + # ─── Silent defaults ─── + [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 + $ProgressPreference = 'SilentlyContinue' + $ConfirmPreference = 'None' + + # ─── Pre-create folder if running as SYSTEM (avoids NuGet install bug) ─── + $provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies" + if (-not (Test-Path $provPath)) { + try { + New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null + Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent + } catch { + Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warning Bootstrap -LogToEvent + } + } + + # ─── Ensure PowerShellGet is available ─── + if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) { + try { + Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop + Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent + } catch { + Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent + } + } + + # ─── Ensure PackageManagement is up-to-date ─── + $pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version + if ($pkgMgmtVersion -lt [Version]"1.3.1") { + try { + Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop + Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent + } catch { + Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent + } + } + + # ─── Import modules silently ─── + Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null + Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null + + # ─── Trust PSGallery if not already ─── + $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue + if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') { + try { + Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop + Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent + } catch { + Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warning Bootstrap -LogToEvent + } + } + + # ─── Ensure NuGet is installed silently ─── + $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue + if (-not $nuget) { + try { + Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop + $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue + Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent + } catch { + Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent + } + } else { + Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent + } + + # ─── Final import check ─── + try { + Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null + } catch { + Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent + } + + #endregion — guarantee NuGet provider is present without prompting + } + + + function Install-SVSMSP { + param ( + [switch] $Cleanup, + [switch] $InstallToolkit, + [Parameter(Mandatory = $false)][array] $AllModules = @(@{ ModuleName = "SVS_Toolkit" }, @{ ModuleName = "SVSMSP" }), + [Parameter(Mandatory = $false)][array] $AllRepositories = @(@{ RepoName = "SVS_Repo" }, @{ RepoName = "SVS_Toolkit" }), + [Parameter(Mandatory = $false)][string] $NewModuleName = "SVSMSP", + [Parameter(Mandatory = $false)][string] $NewRepositoryName = "SVS_Repo", + [Parameter(Mandatory = $false)][string] $NewRepositoryURL = "http://proget.svstools.ca:8083/nuget/SVS_Repo/" + ) + + function Perform-Cleanup { + Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." "Info" "SVSModule" + + # Attempt to uninstall all versions of SVSMSP + try { + Uninstall-Module -Name SVSMSP -AllVersions -Force -ErrorAction Stop + Write-LogHybrid "SVSMSP module uninstalled from system." "Success" "SVSModule" -LogToEvent + } + catch { + # If no module was found, just warn and continue + if ($_.Exception.Message -match 'No match was found') { + Write-LogHybrid "No existing SVSMSP module found to uninstall." "Warning" "SVSModule" -LogToEvent + } + else { + Write-LogHybrid "Failed to uninstall SVSMSP: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent + } + } + + # Remove the custom repository if registered + if (Get-PSRepository -Name SVS_Repo -ErrorAction SilentlyContinue) { + try { + Unregister-PSRepository -Name SVS_Repo -ErrorAction Stop + Write-LogHybrid "SVS_Repo repository unregistered." "Success" "SVSModule" -LogToEvent + } + catch { + Write-LogHybrid "Failed to unregister SVS_Repo: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent + } + } + + # Finally, remove it from the current session if loaded + if (Get-Module -Name SVSMSP) { + try { + Remove-Module SVSMSP -Force -ErrorAction Stop + Write-LogHybrid "SVSMSP module removed from current session." "Success" "SVSModule" -LogToEvent + } + catch { + Write-LogHybrid "Failed to remove SVSMSP from session: $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent + } + } + } + + function Remove-SVSDeploymentRegKey { + $regKey = 'HKLM:\Software\SVS' + + try { + if (Test-Path $regKey) { + Remove-Item -Path $regKey -Recurse -Force + Write-LogHybrid "Registry key '$regKey' deleted successfully." "Success" "SVSModule" -LogToEvent + } + else { + Write-LogHybrid "Registry key '$regKey' not found; nothing to delete." "Info" "SVSModule" -LogToEvent + } + } + catch { + Write-LogHybrid "Failed to delete registry key '$regKey': $($_.Exception.Message)" "Error" "SVSModule" -LogToEvent + } + } + + + + function Perform-ToolkitInstallation { + Initialize-NuGetProvider + Perform-Cleanup + Write-LogHybrid "Registering repo $NewRepositoryName…" "Info" "SVSModule" -LogToEvent + if (-not (Get-PSRepository -Name $NewRepositoryName -ErrorAction SilentlyContinue)) { + Register-PSRepository -Name $NewRepositoryName -SourceLocation $NewRepositoryURL -InstallationPolicy Trusted + } + Write-LogHybrid "Installing module $NewModuleName…" "Info" "SVSModule" -LogToEvent + Install-Module -Name $NewModuleName -Repository $NewRepositoryName -Scope AllUsers -Force + Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" -LogToEvent + } + + Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent + if ($Cleanup) { + + Perform-Cleanup + Remove-SVSDeploymentRegKey + return + + } + if ($InstallToolkit) { + Perform-ToolkitInstallation; return + } + # default if no switch passed: + Perform-ToolkitInstallation + } + + #endregion SVS Module + + + + #region Write-Log + + # Fallback logger used when the SVSMSP module (and its Write-Log) is not available. + # Mirrors the behaviour of the toolkit Write-Log (v1.5), including: + # - Default EventLog: "SVSMSP Events" (out of Application log) + # - Default EventSource: "SVSMSP_Module" + # - Level-based Event IDs and console colors + # - Global in-memory log cache + # - One-time Event Log/source initialization with optional auto-elevation + function Write-LogHelper { + <# + .SYNOPSIS + Standardized logging utility with console/file output and Windows Event Log support, + including one-time event source initialization and optional auto-elevated creation + of a custom log/source. (Fallback implementation for ScriptMonkey.) + + .DESCRIPTION + Mirrors the SVSMSP toolkit Write-Log so that Write-LogHybrid can safely fall back + when the module isn't loaded. + + .NOTES + Default EventLog : SVSMSP Events + Default Source : SVSMSP_Module + #> + [CmdletBinding()] + param ( + [Parameter(Mandatory = $true)] + [string]$Message, + + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level = "Info", + + [string]$TaskCategory = "GeneralTask", + + [switch]$LogToEvent = $false, + + [string]$EventSource = "SVSMSP_Module", + + # Custom log name so you get your own node under "Applications and Services Logs" + [string]$EventLog = "SVSMSP Events", + + [int]$CustomEventID, + + [string]$LogFile, + + [switch]$PassThru + ) + + # ---------- Event ID / console color ---------- + $EventID = if ($CustomEventID) { $CustomEventID } else { + switch ($Level) { + "Info" { 1000 } + "Warning" { 2000 } + "Error" { 3000 } + "Success" { 4000 } + default { 1000 } + } + } + + $Color = switch ($Level) { + "Info" { "Cyan" } + "Warning" { "Yellow" } + "Error" { "Red" } + "Success" { "Green" } + default { "White" } + } + + $FormattedMessage = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" + Write-Host $FormattedMessage -ForegroundColor $Color + + # ---------- In-memory cache ---------- + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + + $logEntry = [PSCustomObject]@{ + Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") + Level = $Level + Message = $FormattedMessage + } + [void]$Global:LogCache.Add($logEntry) + + # ---------- Optional file output ---------- + if ($LogFile) { + try { + "$($logEntry.Timestamp) $FormattedMessage" | + Out-File -FilePath $LogFile -Append -Encoding UTF8 + } + catch { + Write-Host "[Warning] Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + # ---------- Windows Event Log handling with one-time init + optional auto-elevate ---------- + if ($LogToEvent) { + + # Per-run cache for (LogName|Source) init state + if (-not $Global:EventSourceInitState) { + $Global:EventSourceInitState = @{} + } + + $EntryType = switch ($Level) { + "Info" { "Information" } + "Warning" { "Warning" } + "Error" { "Error" } + "Success" { "Information" } # treat success as info in Event Log + default { "Information" } + } + + $sourceKey = "$EventLog|$EventSource" + + if (-not $Global:EventSourceInitState.ContainsKey($sourceKey) -or + -not $Global:EventSourceInitState[$sourceKey]) { + + try { + # Only bother if the source doesn't already exist + if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { + + # Check if current token is admin + $isAdmin = $false + try { + $current = [Security.Principal.WindowsIdentity]::GetCurrent() + $principal = New-Object Security.Principal.WindowsPrincipal($current) + $isAdmin = $principal.IsInRole( + [Security.Principal.WindowsBuiltInRole]::Administrator + ) + } + catch { + $isAdmin = $false + } + + if ($isAdmin) { + # Elevated already: create log/source directly + New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop + } + else { + # Not elevated: run a one-off helper as admin to create log/source + $helperScript = @" +if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) { + New-EventLog -LogName '$EventLog' -Source '$EventSource' +} +"@ + + $tempPath = [System.IO.Path]::Combine( + $env:TEMP, + "Init_${EventLog}_$EventSource.ps1".Replace(' ', '_') + ) + + $helperScript | Set-Content -Path $tempPath -Encoding UTF8 + + try { + # This will trigger UAC prompt in interactive sessions + $null = Start-Process -FilePath "powershell.exe" ` + -ArgumentList "-ExecutionPolicy Bypass -File `"$tempPath`"" ` + -Verb RunAs -Wait -PassThru + } + catch { + Write-Host "[Warning] Auto-elevation to create Event Log '$EventLog' / source '$EventSource' failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + finally { + Remove-Item -Path $tempPath -ErrorAction SilentlyContinue + } + } + } + + # Re-check after creation attempt + if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) { + $Global:EventSourceInitState[$sourceKey] = $true + } + else { + $Global:EventSourceInitState[$sourceKey] = $false + Write-Host "[Warning] Event source '$EventSource' does not exist and could not be created. Skipping Event Log write." -ForegroundColor Yellow + } + } + catch { + Write-Host "[Warning] Failed to initialize Event Log '$EventLog' / source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow + $Global:EventSourceInitState[$sourceKey] = $false + } + } + + # Only write if initialization succeeded + if ($Global:EventSourceInitState[$sourceKey]) { + try { + $EventMessage = "TaskCategory: $TaskCategory | Message: $Message" + Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $EntryType -EventId $EventID -Message $EventMessage + } + catch { + Write-Host "[Warning] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + } + # ------------------------------------------------------------------------------------------ + + if ($PassThru) { + return $logEntry + } + } + + # ───────────────────────────────────────────────────────────────────────── + # WRITE-LOG HYBRID + # Uses module Write-Log if present; otherwise falls back to Write-LogHelper. + # Defaults aligned with toolkit: + # EventSource = "SVSMSP_Module" + # EventLog = "SVSMSP Events" + # ───────────────────────────────────────────────────────────────────────── + function Write-LogHybrid { + [CmdletBinding()] + param( + [Parameter(Mandatory = $true)] + [string]$Message, + + [ValidateSet("Info", "Warning", "Error", "Success", "General")] + [string]$Level = "Info", + + [string]$TaskCategory = "GeneralTask", + + [switch]$LogToEvent, + + [string]$EventSource = "SVSMSP_Module", + + [string]$EventLog = "SVSMSP Events", + + [int]$CustomEventID, + + [string]$LogFile, + + [switch]$PassThru, + + [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] + [string]$ForegroundColorOverride + ) + + $formatted = "[$Level] [$TaskCategory] $Message" + + # Build the common parameter set for forwarding into Write-Log / Write-LogHelper + $invokeParams = @{ + Message = $Message + Level = $Level + TaskCategory = $TaskCategory + LogToEvent = $LogToEvent + EventSource = $EventSource + EventLog = $EventLog + } + + if ($PSBoundParameters.ContainsKey('CustomEventID')) { + $invokeParams.CustomEventID = $CustomEventID + } + if ($PSBoundParameters.ContainsKey('LogFile')) { + $invokeParams.LogFile = $LogFile + } + if ($PassThru) { + $invokeParams.PassThru = $true + } + + if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { + # 1) print to console with the override color + Write-Host $formatted -ForegroundColor $ForegroundColorOverride + + # 2) then forward the call (sans the override) to Write-Log or Write-LogHelper + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log @invokeParams + } + else { + Write-LogHelper @invokeParams + } + } + else { + # No override: let Write-Log / Write-LogHelper handle everything (including console color) + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log @invokeParams + } + else { + Write-LogHelper @invokeParams + } + } + } + + #endregion Write-Log + + + # This function is used as a fallback if the SVSMSP module is not installed + # Should change this "[string]$EventLog = "Application", => [string]$EventLog = "SVS Scripting", " + function Write-LogHelper { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "Script Automation Monkey", + [string]$EventLog = "Application", + [int] $CustomEventID, + [string]$LogFile, + [switch]$PassThru + ) + + # ─── IDs & Colors ──────────────────────────────────────────────── + $idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 } + $colMap = @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" } + $EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] } + $color = $colMap[$Level] + $fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" + + # ─── Console Output ───────────────────────────────────────────── + Write-Host $fmt -ForegroundColor $color + + # ─── In-Memory Cache ───────────────────────────────────────────── + + # ─── In-Memory Cache ───────────────────────────────────────────── + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + $Global:LogCache.Add([pscustomobject]@{ + Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + Level = $Level + Message = $fmt + }) | Out-Null + + + # ─── File Logging ──────────────────────────────────────────────── + if ($PSBoundParameters.LogFile) { + try { + "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" | + Out-File -FilePath $LogFile -Append -Encoding UTF8 + } + catch { + Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow + } + } + + # ─── Event Log ────────────────────────────────────────────────── + if ($LogToEvent) { + try { + # 1) Ensure your custom source/log exist + if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { + New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop + } + } catch { + Write-Host "[Warning] Could not create event log '$EventLog' or source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow + return + } + + # 2) Map level to entry type + $entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' } + + # 3) Write to the Windows event log + try { + Write-EventLog ` + -LogName $EventLog ` + -Source $EventSource ` + -EntryType $entryType ` + -EventID $EventID ` + -Message $fmt + } + catch { + Write-Host "[Warning] EventLog failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + if ($PassThru) { return $Global:LogCache[-1] } + } + + # ───────────────────────────────────────────────────────────────────────── + # WRITE-LOG HYBRID (single definition, chooses at runtime if we use the + # Write-Log from the module or the built-in Write-LogHelper funtions ) + # Should chanfge this "[string]$EventLog = "Application"," => "[string]$EventLog = "SVS Scripting"," + # ───────────────────────────────────────────────────────────────────────── + + function Write-LogHybrid { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "Script Automation Monkey", + [string]$EventLog = "Application", + [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] + [string]$ForegroundColorOverride + ) + + $formatted = "[$Level] [$TaskCategory] $Message" + + if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { + # 1) print to console with the override color + Write-Host $formatted -ForegroundColor $ForegroundColorOverride + + # 2) then forward the call (sans the override) to Write-Log or Write-LogHelper + $invokeParams = @{ + Message = $Message + Level = $Level + TaskCategory = $TaskCategory + LogToEvent = $LogToEvent + EventSource = $EventSource + EventLog = $EventLog + } + + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log @invokeParams + } + else { + Write-LogHelper @invokeParams + } + } + else { + # No override: let Write-Log / Write-LogHelper handle everything (including console color) + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log ` + -Message $Message ` + -Level $Level ` + -TaskCategory $TaskCategory ` + -LogToEvent:$LogToEvent ` + -EventSource $EventSource ` + -EventLog $EventLog + } + else { + Write-LogHelper ` + -Message $Message ` + -Level $Level ` + -TaskCategory $TaskCategory ` + -LogToEvent:$LogToEvent ` + -EventSource $EventSource ` + -EventLog $EventLog + } + } + } + + + #endregion Write-Log + + #region building the Menus + + # Define every task once here: + # Id → checkbox HTML `id` + # Name → URL path (`/Name`) + # Label → user-visible text + # HandlerFn → the PowerShell function to invoke + # Page → which tab/page it appears on + + $Global:SamyTasks = @( + # On-Boarding, left column + @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' }, + @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' }, + @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' }, + @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' }, + @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' }, + @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' }, + @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left'; + SubOptions= @( + @{ Value='inputVar'; Label='Copy Site Variables' }, + @{ Value='rmm'; Label='Install RMM Agent' }, + @{ Value='exe'; Label='Download Executable' } + ) + }, + + + # On-Boarding, right column (optional bits) + @{ Id='enableBitLocker'; Name='EnableBitLocker'; Label='Enable BitLocker'; HandlerFn='Set-SVSBitLocker'; Page='onboard'; Column='right' }, + @{ Id='setEdgeDefaultSearch';Name='setedgedefaultsearch';Label='Set Edge Default Search'; Tooltip='Will configure Edge to use Google as default search provider'; HandlerFn='set-EdgeDefaultSearchProvider';Page='onboard'; Column='right' }, + + # Off-Boarding + @{ Id='offUninstallCyberQP'; Name='offUninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Handle-UninstallCyberQP'; Page='offboard' }, + @{ Id='offUninstallSVSHelpDesk'; Name='offUninstallSVSHelpDesk'; Label='Uninstall SVS HelpDesk'; HandlerFn='Handle-UninstallSVSHelpDesk'; Page='offboard' }, + @{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker'; HandlerFn='Handle-UninstallThreatLocker'; Page='offboard' }, + @{ Id='offUninstallRocketCyber'; Name='offUninstallRocketCyber'; Label='Uninstall RocketCyber'; HandlerFn='Handle-UninstallRocketCyber'; Page='offboard' }, + @{ Id='offCleanupSVSMSPModule'; Name='offCleanupSVSMSPModule'; Label='Cleanup SVSMSP Toolkit'; HandlerFn='Handle-CleanupSVSMSP'; Page='offboard' }, + + # Tweaks + @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' }, + + # SVS Apps + @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }, + @{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' }, + @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' } + + ) +Write-LogHybrid "Tasks by page: onboard=$( + ($Global:SamyTasks | Where-Object Page -eq 'onboard').Count +) offboard=$( + ($Global:SamyTasks | Where-Object Page -eq 'offboard').Count +) tweaks=$( + ($Global:SamyTasks | Where-Object Page -eq 'tweaks').Count +) apps=$( + ($Global:SamyTasks | Where-Object Page -eq 'SVSApps').Count +)" Info UI -LogToEvent + + #endregion building the Menus + + #region Build-Checkboxes + function Build-Checkboxes { + param( + [Parameter(Mandatory)][string]$Page, + [string]$Column + ) + + # Start with all tasks on the given page + $tasks = $Global:SamyTasks | Where-Object Page -EQ $Page + + # Only filter by Column when it actually matters (onboard left/right) + if (-not [string]::IsNullOrEmpty($Column)) { + $tasks = $tasks | Where-Object Column -EQ $Column + } + + ( + $tasks | + ForEach-Object { + $taskId = $_.Id + $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) { + " title='$($_.Tooltip)'" + } else { '' } + + $html = "" + + if ($_.SubOptions) { + $subHtml = ( + $_.SubOptions | + ForEach-Object { + "" + } + ) -join "`n" + + $html += @" +
+"@ + } + + $html + } + ) -join "`n" + } # end function Build-checkboxes + + + #endregion Build-Checkboxes + + #region Get-ModuleVersionHtml + + ### Get SVSMSP module version to display in the UI + function Get-ModuleVersionHtml { + $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1 + if ($mod) { + return "