diff --git a/samy.ps1 b/samy.ps1
deleted file mode 100644
index 3c463ed..0000000
--- a/samy.ps1
+++ /dev/null
@@ -1,2531 +0,0 @@
-#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
-
-function Invoke-ScriptAutomationMonkey {
-
-<#
-.SYNOPSIS
- Main entry point for the Script Automation Monkey (SAMY) deployment tool.
-
-.DESCRIPTION
- Starts the SAMY web UI or runs one of several headless modes for installing,
- cleaning up, or offboarding the SVSMSP toolkit and related stack.
-
- Headless modes:
- - Silent install of the SVSMSP toolkit (no UI)
- - Toolkit cleanup and removal
- - Full offboarding of the SVSMSP stack
- - Datto RMM integration for:
- * Site discovery and export
- * Site variable push to registry
- * Agent download and installation
- * Saving a copy of the installer
- - Printer and device helpers exposed through the UI endpoints
-
- When called with no parameters (or via iwr samy.svstools.ca | iex),
- SAMY starts a local HTTP listener, renders the web UI, and executes
- tasks selected in the browser.
-
-.PARAMETER SilentInstall
- Run a default headless install of the SVSMSP toolkit without showing the UI.
-
-.PARAMETER Cleanup
- Remove the SVSMSP toolkit, custom repository, and related deployment
- artifacts without launching the UI.
-
-.PARAMETER Offboard
- Perform a headless offboard of the SVSMSP stack:
- uninstall integrated tools and then clean up the SVSMSP toolkit.
-
-.PARAMETER UseWebhook
- Use the configured webhook to retrieve Datto RMM API credentials instead
- of passing ApiUrl / ApiKey / ApiSecretKey directly.
-
-.PARAMETER WebhookPassword
- Password or token used to authenticate to the Datto credential webhook.
-
-.PARAMETER ApiUrl
- Base URL for the Datto RMM API when using direct API mode.
-
-.PARAMETER ApiKey
- Datto RMM API key when using direct API mode.
-
-.PARAMETER ApiSecretKey
- Datto RMM API secret key when using direct API mode.
-
-.PARAMETER FetchSites
- Fetch the list of Datto RMM sites. Can be combined with SaveSitesList
- and OutputFile to export the site list.
-
-.PARAMETER SaveSitesList
- When used with FetchSites, save the Datto site list to the file
- specified by OutputFile (CSV or JSON).
-
-.PARAMETER OutputFile
- Target file name for the Datto site list when SaveSitesList is used.
- Supports .csv and .json. Default is datto_sites.csv.
-
-.PARAMETER SiteUID
- Datto RMM site UID used when pushing site variables or installing
- the RMM agent for a specific site.
-
-.PARAMETER SiteName
- Friendly Datto RMM site name used for logging only.
-
-.PARAMETER PushSiteVars
- Fetch Datto RMM site variables and push them into the HKLM:\Software\SVS
- Deployment registry key.
-
-.PARAMETER InstallRMM
- Download and run the Datto RMM agent installer for the selected site.
-
-.PARAMETER SaveCopy
- Download and save a copy of the Datto RMM agent installer to disk
- without launching it.
-
-.EXAMPLE
- iwr samy.svstools.ca | iex
-
- Download and launch SAMY in interactive UI mode on the local machine.
-
-.EXAMPLE
- & ([ScriptBlock]::Create((iwr 'samybeta.svstools.ca').Content )) -SilentInstall
- (iwr samy.svstools.ca -UseBasicParsing).Content | iex
- Invoke-ScriptAutomationMonkey -SilentInstall
-
- Run a headless install of the SVSMSP toolkit (no UI) from the current session.
-
-.EXAMPLE
- (iwr samy.svstools.ca -UseBasicParsing).Content | iex
- Invoke-ScriptAutomationMonkey `
- -UseWebhook `
- -WebhookPassword 'MySecret' `
- -FetchSites `
- -SaveSitesList `
- -OutputFile 'sites.json'
-
- Fetch Datto RMM sites via webhook and save them to sites.json.
-
-#>
-
- # ==============================================================
- # PARAMETERS (DRIVE MODES / PARAMETER SETS)
- #
- # Default UI:
- # & (iwr samy.svstools.ca | iex)
- #
- # Headless modes:
- # -SilentInstall Default SVSMSP toolkit install
- # -Cleanup Remove toolkit, repo, registry
- # -Offboard Full headless offboarding
- #
- # Datto fetch (sites only):
- # Webhook:
- # -UseWebhook -WebhookPassword 'pwd' -FetchSites `
- # [-SaveSitesList] [-OutputFile 'sites.json']
- #
- # Direct:
- # -ApiUrl 'https://api.example.com' `
- # -ApiKey 'YourApiKey' -ApiSecretKey 'YourSecretKey' `
- # -FetchSites [-SaveSitesList] [-OutputFile 'sites.json']
- #
- # Datto install (single site actions):
- # Webhook:
- # -UseWebhook -WebhookPassword 'pwd' `
- # -SiteUID 'site-123' -SiteName 'Acme Corp' `
- # [-PushSiteVars] [-InstallRMM] [-SaveCopy]
- #
- # Direct:
- # -ApiUrl 'https://api.example.com' -ApiKey 'k' -ApiSecretKey 's' `
- # -SiteUID 'site-123' -SiteName 'Acme Corp' `
- # [-PushSiteVars] [-InstallRMM] [-SaveCopy]
- # ==============================================================
-
- [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 (shared switches)
-
- [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
- )
-
- # 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()
- }
-
- # SVS Module
-
- function Initialize-NuGetProvider {
- [CmdletBinding()]
- param()
-
- # Silent defaults
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- $ProgressPreference = 'SilentlyContinue'
- $ConfirmPreference = 'None'
-
- # Pre-create folder if running as SYSTEM
- $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
- }
- }
-
- 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 ($_.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
- }
- }
-
- # Remove from current session
- 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
-
- 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
- }
-
- 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
-
- 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
-
- 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
- }
-
- # Write-Log (fallback + hybrid)
-
- function Write-LogHelper {
- <#
- .SYNOPSIS
- Fallback logging utility with console/file output and Windows Event Log support.
- #>
- [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",
-
- [string]$EventLog = "SVSMSP Events",
-
- [int]$CustomEventID,
-
- [string]$LogFile,
-
- [switch]$PassThru
- )
-
- $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
-
- 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)
-
- 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
- }
- }
-
- if ($LogToEvent) {
-
- if (-not $Global:EventSourceInitState) {
- $Global:EventSourceInitState = @{}
- }
-
- $EntryType = switch ($Level) {
- "Info" { "Information" }
- "Warning" { "Warning" }
- "Error" { "Error" }
- "Success" { "Information" }
- default { "Information" }
- }
-
- $sourceKey = "$EventLog|$EventSource"
-
- if (-not $Global:EventSourceInitState.ContainsKey($sourceKey) -or
- -not $Global:EventSourceInitState[$sourceKey]) {
-
- try {
- if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
-
- $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) {
- New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
- }
- else {
- $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 {
- $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
- }
- }
- }
-
- 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
- }
- }
-
- 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
- }
- }
-
- 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"
-
- $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')) {
- Write-Host $formatted -ForegroundColor $ForegroundColorOverride
-
- if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
- Write-Log @invokeParams
- }
- else {
- Write-LogHelper @invokeParams
- }
- }
- else {
- if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
- Write-Log @invokeParams
- }
- else {
- Write-LogHelper @invokeParams
- }
- }
- }
-
- # Computer rename helper
-
- 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
- }
-
- # Task menu definition
-
- $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
- @{ 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
-
- function Publish-Checkboxes {
- param(
- [Parameter(Mandatory)][string]$Page,
- [string]$Column
- )
-
- $tasks = $Global:SamyTasks | Where-Object Page -EQ $Page
-
- 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 += @"
-
-$subHtml
-
-"@
- }
-
- $html
- }
- ) -join "`n"
- }
-
- function Get-ModuleVersionHtml {
- $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1
-
- $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
"
- }
-
- # Server helpers
-
- function Get-NextFreePort {
- param([int]$Start = $Port)
- for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) {
- $l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
- try {
- $l.Start();
- $l.Stop();
- return $p
- } catch {}
- }
- throw "No free TCP port available."
- }
-
- 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
- }
- }
-
- # UI HTML helpers
-
- 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' }
-
- $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 ''
-
- $tasksJsAll = (
- $Global:SamyTasks | ForEach-Object {
- " { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
- }
- ) -join ",`n"
-
- $branchDisplay = switch ($Script:SamyBranch.ToLower()) {
- 'main' { 'Main / Stable' }
- 'beta' { 'Beta' }
- default { $Script:SamyBranch }
- }
-
- $cssContent = Get-RemoteText -Url $Script:SamyCssUrl
- $jsContent = Get-RemoteText -Url $Script:SamyJsUrl
-
- if ($cssContent) {
- $pattern = 'background-image:\s*url\("SAMY\.png"\);?'
- $replacement = "background-image: url('$Script:SamyBgLogoUrl');"
- $cssContent = [regex]::Replace($cssContent, $pattern, $replacement)
- }
-
- $htmlTemplate = @"
-
-
-
-
-
-Script Automation Monkey
-
-
-
-
-
-
-
-

- {{moduleVersion}}
-
-
-
- Script Automation Monkey (Yeah!)
-
-
-
-
-
-
-
-
-
-
On-Boarding
-
This new deployment method ensures everything is successfully deployed with greater ease!
-
-
-
-
SVSMSP Stack
-
- {{onboardLeftColumn}}
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Off-Boarding
-
-
-
Remove Stack
-
- {{offboardCheckboxes}}
-
-
-
-
-
-
Tweaks
-
-
-
Tweaks
- {{tweaksCheckboxes}}
-
-
-
-
-
-
SVS APPs
-
-
-
Applications
- {{appsCheckboxes}}
-
-
-
-
-
-
Devices
-
Manage printers and other client devices.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Check the printers to install, and mark one as "Make default" (optional).
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-"@
-
- $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
- }
-
- # 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 {
- if ($null -eq $Object) {
- Write-LogHybrid "Send-JSON called with `$null object; returning empty JSON array." Warning Printers -LogToEvent
- $json = '[]'
- }
- else {
- 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 = '[]'
- }
- }
-
- $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 {
- 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 {
- }
- }
- }
-
- function Invoke-TasksCompleted {
- param($Context)
-
- Write-LogHybrid "All UI-selected tasks processed" Info UI -LogToEvent
- Send-Text $Context "Tasks completion acknowledged."
- }
-
- # Datto handlers (HTTP side)
-
- function Invoke-FetchSites {
- param($Context)
-
- try {
- $raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
- $pw = (ConvertFrom-Json $raw).password
-
- $Global:WebhookPassword = $pw
-
- $sites = Install-DattoRMM `
- -UseWebhook `
- -WebhookPassword $pw `
- -FetchSites `
- -SaveSitesList:$SaveSitesList `
- -OutputFile $OutputFile
-
- Send-JSON $Context $sites
- }
- catch {
- Write-LogHybrid "Invoke-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- $Context.Response.StatusCode = 500
- Send-Text $Context "Internal server error fetching sites."
- }
- }
-
- # Onboarding handlers
-
- function Invoke-SetSVSPowerPlan {
- param($Context)
-
- Set-SVSPowerPlan
-
- 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)
-
- Install-CyberQP
-
- Write-LogHybrid "CyberQP installed" Success OnBoard
- Send-Text $Context "CyberQP installed"
- }
-
- function Invoke-InstallThreatLocker {
- param($Context)
-
- Install-ThreatLocker
-
- Write-LogHybrid "ThreatLocker installed" Success OnBoard
- Send-Text $Context "ThreatLocker installed"
- }
-
- function Invoke-InstallRocketCyber {
- param($Context)
-
- Install-RocketCyber
-
- Write-LogHybrid "RocketCyber installed" Success OnBoard
- Send-Text $Context "RocketCyber installed"
- }
-
- function Invoke-InstallHelpDesk {
- param($Context)
-
- Install-svsHelpDesk
-
- 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
- }
-
- $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."
- }
- }
-
- function Invoke-InstallDattoRMM {
- param($Context)
-
- try {
- if ($Context.Request.HttpMethod -ne 'POST') {
- $Context.Response.StatusCode = 405
- Send-Text $Context 'Use POST'
- return
- }
-
- $body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
- $data = ConvertFrom-Json $body
-
- Install-DattoRMM `
- -UseWebhook `
- -WebhookPassword $Global:WebhookPassword `
- -SiteUID $data.UID `
- -SiteName $data.Name `
- -PushSiteVars:($data.checkedValues -contains 'inputVar') `
- -InstallRMM: ($data.checkedValues -contains 'rmm') `
- -SaveCopy: ($data.checkedValues -contains 'exe')
-
- Send-Text $Context "Triggered DattoRMM for $($data.Name)"
- }
- catch {
- Write-LogHybrid "Invoke-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- $Context.Response.StatusCode = 500
- Send-Text $Context "Internal server error during DattoRMM install."
- }
- }
-
- # 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)"
- }
- }
-
- # Offboarding handlers
-
- function Invoke-UninstallCyberQP {
- param($Context)
-
- try {
- if (Get-Command Uninstall-CyberQP -ErrorAction Stop) {
- Uninstall-CyberQP
- Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent
- if ($Context) { 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
- if ($Context) { 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
- if ($Context) { 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
- if ($Context) { 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
- if ($Context) { 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
- if ($Context) { 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
- if ($Context) { 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
- if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
- }
- }
-
- function Invoke-CleanupSVSMSP {
- param($Context)
-
- try {
- if (Get-Command Install-SVSMSP -ErrorAction Stop) {
- Install-SVSMSP -Cleanup
-
- Write-LogHybrid "SVSMSP toolkit cleanup completed (module, repo, registry)." Success OffBoard -LogToEvent
- if ($Context) { 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
- if ($Context) { Send-Text $Context "ERROR: $($_.Exception.Message)" }
- }
- }
-
- # Printer handlers and core
-
- 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
-
- 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.PSObject.Properties.Name -contains 'DriverPackageUrl' -and $Profile.DriverPackageUrl) {
- return $Profile.DriverPackageUrl
- }
-
- if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
- return "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
- }
-
- return $null
- }
-
- function Get-SamyClientListFromServer {
- [CmdletBinding()]
- param(
- [Parameter(Mandatory)][string]$Uri,
- [Parameter(Mandatory)][string]$Password
- )
-
- try {
- Write-LogHybrid "Calling client list service at $Uri" Info Printers -LogToEvent
-
- $headers = @{
- SAMYPW = $Password
- }
-
- $resp = Invoke-RestMethod -Uri $Uri `
- -Method Get `
- -Headers $headers `
- -ContentType 'application/json' `
- -ErrorAction Stop
-
- if (-not $resp) {
- Write-LogHybrid "Client list service returned no data." Warning Printers -LogToEvent
- return @()
- }
-
- 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
- }
-
- $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
- if (-not $password) {
- $Context.Response.StatusCode = 400
- Send-Text $Context 'Password is required.'
- return
- }
-
- $uri = 'https://bananas.svstools.ca/getprinters'
- Write-LogHybrid "Fetching printers from $uri" Info Printers -LogToEvent
-
- $printers = Get-SamyClientListFromServer -Uri $uri -Password $password
-
- if ($null -eq $printers) {
- Write-LogHybrid "Get-SamyClientListFromServer returned `$null; sending empty JSON array." Warning Printers -LogToEvent
- $printers = @()
- }
-
- try {
- Update-SamyPrinterConfig -PrinterProfiles $printers -SkipIfEmpty
- }
- catch {
- Write-LogHybrid "Update-SamyPrinterConfig failed: $($_.Exception.Message)" Warning Printers -LogToEvent
- }
-
- 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) {
- $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 {
- 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."
- }
- }
-
- function Get-SamyPrinterLocalConfigPath {
- [CmdletBinding()]
- param()
-
- $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
- }
-
- $Script:Samy_PrinterProfiles = $null
-
- function Get-SamyPrinterProfiles {
- [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 {
- [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."
- }
-
- $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
-
- $localDriverRoot = Get-SamyDriverFolderForProfile -Profile $Profile
-
- $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
- }
- }
-
- $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) {
- Write-LogHybrid "Driver package not found at $driverPackageUrl (404). Falling back to INF-only install for '$($Profile.DisplayName)'." Warning Printers -LogToEvent
- }
- 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
- }
-
- 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 (-not $infPath) {
- if ($Profile.PSObject.Properties.Name -contains 'DriverInfName' -and $Profile.DriverInfName) {
-
- $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' not found at root; searching recursively..." Warning Printers -LogToEvent
-
- $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
- }
- }
- }
-
- 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
-
- $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'."
- }
-
- try {
- Write-LogHybrid "Calling Add-PrinterDriver -Name '$driverName'." 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)"
- }
-
- 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 {
- [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 {
- [CmdletBinding()]
- param(
- [Parameter(Mandatory)]
- [object]$PrinterProfiles,
-
- [switch]$SkipIfEmpty
- )
-
- $path = Get-SamyPrinterLocalConfigPath
-
- $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
-
- $Script:Samy_PrinterProfiles = $null
- }
- catch {
- Write-LogHybrid "Failed to write printers.json to '$path': $($_.Exception.Message)" Error Printers -LogToEvent
- }
- }
-
- # Install-DattoRMM core function
-
- function Install-DattoRMM {
- <#
- .SYNOPSIS
- Installs/configures the Datto RMM agent and handles sites/variables.
- #>
- [CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium')]
- param (
- [switch]$UseWebhook,
- [string]$WebhookPassword,
- [string]$WebhookUrl = $Global:DattoWebhookUrl,
- [string]$ApiUrl,
- [string]$ApiKey,
- [string]$ApiSecretKey,
- [switch]$FetchSites,
- [switch]$SaveSitesList,
- [string]$OutputFile = 'datto_sites.csv',
- [switch]$PushSiteVars,
- [switch]$InstallRMM,
- [switch]$SaveCopy,
- [string]$SiteUID,
- [string]$SiteName
- )
-
- if ($SaveSitesList -and -not $FetchSites) {
- Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM -LogToEvent
- return
- }
-
- if ($UseWebhook) {
- if (-not $WebhookPassword) {
- Write-LogHybrid "Webhook password missing." Error DattoRMM -LogToEvent
- return
- }
- try {
- $resp = Invoke-RestMethod -Uri $WebhookUrl `
- -Headers @{ SAMYPW = $WebhookPassword } `
- -Method GET
- $ApiUrl = $resp.ApiUrl
- $ApiKey = $resp.ApiKey
- $ApiSecretKey = $resp.ApiSecretKey
- Write-LogHybrid "Webhook credentials fetched." Success DattoRMM -LogToEvent
- } catch {
- Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- return
- }
- }
-
- if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
- Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent
- return
- }
-
- [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
- try {
- $publicCred = New-Object System.Management.Automation.PSCredential(
- 'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force)
- )
- $tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" `
- -Credential $publicCred `
- -Method Post `
- -ContentType 'application/x-www-form-urlencoded' `
- -Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey"
- $token = $tokenResp.access_token
- Write-LogHybrid "OAuth token acquired." Success DattoRMM -LogToEvent
- } catch {
- Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- return
- }
-
- $headers = @{ Authorization = "Bearer $token" }
-
- if ($FetchSites) {
- try {
- $sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers
- $siteList = $sitesResp.sites | 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 @()
- }
- }
-
- if ($PushSiteVars) {
- try {
- $varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers
- Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM -LogToEvent
- } catch {
- Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- }
- $regPath = "HKLM:\Software\SVS\Deployment"
- foreach ($v in $varsResp.variables) {
- try {
- if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null }
- New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null
- Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM -LogToEvent
- } catch {
- Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent
- }
- }
- }
-
- if ($InstallRMM) {
- if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) {
- try {
- $dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
- $tmp = "$env:TEMP\AgentInstall.exe"
- Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing
- Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM -LogToEvent
- Start-Process -FilePath $tmp -NoNewWindow
- Write-LogHybrid "RMM agent installer launched." Success DattoRMM -LogToEvent
- } catch {
- Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
- }
- }
- }
-
- if ($SaveCopy) {
- 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
- }
- }
-
- if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
- Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning DattoRMM -LogToEvent
- }
- }
-
- # Dispatch-Request
-
- function Dispatch-Request {
- param($Context)
-
- $path = $Context.Request.Url.AbsolutePath.TrimStart('/')
-
- if ($path -eq 'quit') {
- Write-LogHybrid "Shutdown requested" Info Server -LogToEvent
- Send-Text $Context "Server shutting down."
- $Global:Listener.Stop()
- return
- }
-
- if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'tasksCompleted') {
- Invoke-TasksCompleted $Context
- return
- }
-
- if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
- Invoke-FetchSites $Context
- return
- }
-
- if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'renameComputer') {
- Invoke-RenameComputer $Context
- return
- }
-
- 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
- }
-
- if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps', 'devices')) {
- $page = if ($path -eq '') { 'onboard' } else { $path }
- $html = Get-UIHtml -Page $page
- Send-HTML $Context $html
- return
- }
-
- $task = $Global:SamyTasks | Where-Object Name -EQ $path
- if ($task) {
- & $task.HandlerFn $Context
- return
- }
-
- $Context.Response.StatusCode = 404
- Send-Text $Context '404 - Not Found'
- }
-
- # MAIN LOGIC (Toolkit vs DattoFetch vs DattoInstall vs UI)
-
- Write-LogHybrid "Invoke-ScriptAutomationMonkey starting (ParameterSet=$($PSCmdlet.ParameterSetName))" Info Bootstrap -LogToEvent
-
- switch ($PSCmdlet.ParameterSetName) {
-
- 'Toolkit' {
- Write-LogHybrid "Running in Toolkit mode (-SilentInstall)." Info SVSModule -LogToEvent
-
- Install-SVSMSP -InstallToolkit
-
- Write-LogHybrid "Toolkit mode completed." Success SVSModule -LogToEvent
- return
- }
-
- 'Cleanup' {
- Write-LogHybrid "Running in Cleanup mode (-Cleanup)." Info SVSModule -LogToEvent
-
- Install-SVSMSP -Cleanup
-
- Write-LogHybrid "Cleanup mode completed." Success SVSModule -LogToEvent
- return
- }
-
- 'Offboard' {
- Write-LogHybrid "Running in headless Offboard mode (-Offboard)." Info OffBoard -LogToEvent
-
- try { Invoke-UninstallCyberQP -Context $null } catch { Write-LogHybrid "Headless offboard: CyberQP uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
- try { Invoke-UninstallHelpDesk -Context $null } catch { Write-LogHybrid "Headless offboard: HelpDesk uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
- try { Invoke-UninstallThreatLocker -Context $null } catch { Write-LogHybrid "Headless offboard: ThreatLocker uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
- try { Invoke-UninstallRocketCyber -Context $null } catch { Write-LogHybrid "Headless offboard: RocketCyber uninstall failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
- try { Invoke-CleanupSVSMSP -Context $null } catch { Write-LogHybrid "Headless offboard: SVSMSP cleanup failed: $($_.Exception.Message)" Error OffBoard -LogToEvent }
-
- Write-LogHybrid "Headless Offboard mode completed." Success OffBoard -LogToEvent
- return
- }
-
- 'DattoFetch' {
- Write-LogHybrid "Running in DattoFetch mode (fetch sites)." Info DattoRMM -LogToEvent
-
- $sites = Install-DattoRMM `
- -UseWebhook `
- -WebhookPassword $WebhookPassword `
- -FetchSites `
- -SaveSitesList:$SaveSitesList `
- -OutputFile $OutputFile
-
- if ($sites) {
- Write-LogHybrid "DattoFetch returned $($sites.Count) sites." Success DattoRMM -LogToEvent
-
- try {
- $sites | Sort-Object Name | Format-Table Name, UID -AutoSize
- } catch {
- Write-LogHybrid "Failed to render Datto site table to console: $($_.Exception.Message)" Warning DattoRMM -LogToEvent
- }
- } else {
- Write-LogHybrid "DattoFetch returned no sites." Warning DattoRMM -LogToEvent
- }
-
- return
- }
-
- 'DattoInstall' {
- Write-LogHybrid "Running in DattoInstall mode for site '$SiteName' ($SiteUID)." Info DattoRMM -LogToEvent
-
- Install-DattoRMM `
- -UseWebhook `
- -WebhookPassword $WebhookPassword `
- -SiteUID $SiteUID `
- -SiteName $SiteName `
- -PushSiteVars:$PushSiteVars `
- -InstallRMM:$InstallRMM `
- -SaveCopy:$SaveCopy
-
- Write-LogHybrid "DattoInstall mode completed for site '$SiteName'." Success DattoRMM -LogToEvent
- return
- }
-
- default {
- Write-LogHybrid "Starting Script Automation Monkey UI on port $Port." Info UI -LogToEvent
-
- try {
- Start-Process "http://localhost:$Port/" | Out-Null
- } catch {
- Write-LogHybrid "Failed to launch browser: $($_.Exception.Message)" Warning UI -LogToEvent
- }
-
- Start-Server
-
- Write-LogHybrid "Script Automation Monkey UI stopped (listener exited)." Info UI -LogToEvent
- }
- }
-
-} # end function Invoke-ScriptAutomationMonkey
-
-if ($MyInvocation.InvocationName -ne '.') {
- Invoke-ScriptAutomationMonkey @args
-}