#region changes to be done #endregion changes to be done ## Last changes made should fix the issues we had when running this 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 and install - downloads the Datto RMM agent installer and launches it. - Installer archiving - saves a copy of the downloaded installer to C:\Temp. - HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch to log errors and return proper HTTP 500 responses on failure. - Idempotent and WhatIf support - uses ShouldProcess for safe, testable agent installs. Throughout, secrets are never written to logs or console, and all operations produce clear success/failure messages via Write-LogHybrid. .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -UseWebhook ` -WebhookPassword 'pwd' ` -SiteUID 'site-123' ` -SiteName 'Acme Corp' ` -PushSiteVars ` -InstallRMM .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 .EXAMPLE & ([ScriptBlock]::Create( (iwr 'https://sm.svstools.com' -UseBasicParsing).Content )) ` -UseWebhook ` -WebhookPassword 'pwd' ` -FetchSites ` -SaveSitesList ` -OutputFile 'sites.json' .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 .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 .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 + GLOBALS # --------------------------------------------------------------------- [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: shared webhook bits [Parameter(Mandatory, ParameterSetName = 'DattoFetch')] [Parameter(Mandatory, ParameterSetName = 'DattoInstall')] [switch]$UseWebhook, [Parameter(Mandatory, ParameterSetName = 'DattoFetch')] [Parameter(Mandatory, ParameterSetName = 'DattoInstall')] [string]$WebhookPassword, [string]$WebhookUrl = $Global:DattoWebhookUrl, # DattoFetch only [Parameter(ParameterSetName = 'DattoFetch')] [switch]$FetchSites, [Parameter(ParameterSetName = 'DattoFetch')] [switch]$SaveSitesList, [Parameter(ParameterSetName = 'DattoFetch')] [ValidatePattern('\.csv$|\.json$')] [string]$OutputFile = 'datto_sites.csv', # DattoInstall only [Parameter(Mandatory, ParameterSetName = 'DattoInstall')] [string]$SiteUID, [Parameter(Mandatory, ParameterSetName = 'DattoInstall')] [string]$SiteName, [Parameter(ParameterSetName = 'DattoInstall')] [switch]$PushSiteVars, [Parameter(ParameterSetName = 'DattoInstall')] [switch]$InstallRMM, [Parameter(ParameterSetName = 'DattoInstall')] [switch]$SaveCopy ) # --------------------------------------------------------------------- # GLOBAL VARIABLES # --------------------------------------------------------------------- $Port = 8082 $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' 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() [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' $provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies" if (-not (Test-Path $provPath)) { try { New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warning Bootstrap -LogToEvent } } if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) { try { Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent } } $pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version if ($pkgMgmtVersion -lt [Version]"1.3.1") { try { Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent } } Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null $gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') { try { Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warning Bootstrap -LogToEvent } } $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue if (-not $nuget) { try { Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop $nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent } catch { Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent } } else { Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent } 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 Perform-Cleanup { Write-LogHybrid "Cleanup mode enabled. Starting cleanup..." Info SVSModule 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 } } 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 } } 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 } Perform-ToolkitInstallation } # ===================================================================== # WRITE-LOG (primary implementation) # ===================================================================== function Write-LogHelper { <# .SYNOPSIS Standardized logging utility with console/file output and Windows Event Log support. .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", [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 } } } # ===================================================================== # SAMY TASK DEFINITIONS # ===================================================================== $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 # ===================================================================== # BUILD-CHECKBOXES # ===================================================================== function Build-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 = " $($_.Label)" if ($_.SubOptions) { $subHtml = ( $_.SubOptions | ForEach-Object { "" } ) -join "`n" $html += @" "@ } $html } ) -join "`n" } # ===================================================================== # MODULE VERSION HTML # ===================================================================== function Get-ModuleVersionHtml { $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1 if ($mod) { return "
Module Version: $($mod.Version)
" } 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 } } # ===================================================================== # REMOTE TEXT FETCH (GITEA) + UI HTML # ===================================================================== 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 = Build-Checkboxes -Page 'onboard' -Column 'left' $onboardRight = Build-Checkboxes -Page 'onboard' -Column 'right' $offboard = Build-Checkboxes -Page 'offboard' -Column '' $tweaks = Build-Checkboxes -Page 'tweaks' -Column '' $apps = Build-Checkboxes -Page 'SVSApps' -Column '' $tasksJsAll = ( $Global:SamyTasks | ForEach-Object { " { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }" } ) -join ",`n" $styleContent = Get-RemoteText -Url "https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/samy.css" $scriptContent = Get-RemoteText -Url "https://git.svstools.ca/SVS_Public_Repo/S.A.M.Y/raw/branch/main/samy.js" $htmlTemplate = @" Script Monkey
SVS Logo {{moduleVersion}}
Script Automation Monkey (Yeah!)

On-Boarding

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

SVSMSP Stack

{{onboardLeftColumn}}

Optional

{{onboardRightColumn}}

Off-Boarding

Remove Stack

{{offboardCheckboxes}}

Tweaks

Tweaks

{{tweaksCheckboxes}}

SVS APPs

Applications

{{appsCheckboxes}}
"@ $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) return $html } # ===================================================================== # HTTP HELPERS AND HANDLERS # ===================================================================== function Respond-Text { param($Context, $Text) $bytes = [Text.Encoding]::UTF8.GetBytes($Text) $Context.Response.ContentType = 'text/plain' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) $Context.Response.OutputStream.Close() } function Respond-HTML { [CmdletBinding()] param( [Parameter(Mandatory = $true)][object]$Context, [Parameter(Mandatory = $true)][string]$Html ) $bytes = [Text.Encoding]::UTF8.GetBytes($Html) $Context.Response.ContentType = 'text/html' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes, 0, $bytes.Length) $Context.Response.OutputStream.Close() } function Respond-JSON { param($Context, $Object) $json = $Object | ConvertTo-Json -Depth 5 $bytes = [Text.Encoding]::UTF8.GetBytes($json) $Context.Response.ContentType = 'application/json' $Context.Response.ContentLength64 = $bytes.Length $Context.Response.OutputStream.Write($bytes,0,$bytes.Length) $Context.Response.OutputStream.Close() } function Handle-FetchSites { param($Context) try { $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 Respond-JSON $Context $sites } catch { Write-LogHybrid "Handle-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent $Context.Response.StatusCode = 500 Respond-Text $Context "Internal server error fetching sites." } } function Handle-SetSVSPowerPlan { param($Context) Set-SVSPowerPlan Write-LogHybrid "PowerPlan set" Success OnBoard Respond-Text $Context "PowerPlan applied" } function Handle-InstallSVSMSP { param($Context) Write-LogHybrid "HTTP trigger: Handle-InstallSVSMSP" Info OnBoard try { Install-SVSMSP -InstallToolkit Respond-Text $Context "SVSMSP Module installed/updated." } catch { Write-LogHybrid "Error in Install-SVSMSP: $_" Error OnBoard Respond-Text $Context "ERROR: $_" } } function Handle-InstallCyberQP { param($Context) Install-CyberQP Write-LogHybrid "CyberQP installed" Success OnBoard Respond-Text $Context "CyberQP installed" } function Handle-InstallThreatLocker { param($Context) Install-ThreatLocker Write-LogHybrid "ThreatLocker installed" Success OnBoard Respond-Text $Context "ThreatLocker installed" } function Handle-InstallRocketCyber { param($Context) Install-RocketCyber Write-LogHybrid "RocketCyber installed" Success OnBoard Respond-Text $Context "RocketCyber installed" } function Handle-InstallSVSHelpDesk { param($Context) Install-SVSHelpDesk Write-LogHybrid "SVS HelpDesk installed" Success OnBoard Respond-Text $Context "SVS HelpDesk installed" } function Handle-InstallDattoRMM { param($Context) try { if ($Context.Request.HttpMethod -ne 'POST') { $Context.Response.StatusCode = 405 Respond-Text $Context 'Use POST' return } $body = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd() $data = ConvertFrom-Json $body Install-DattoRMM ` -UseWebhook ` -WebhookPassword $Global:WebhookPassword ` -SiteUID $data.UID ` -SiteName $data.Name ` -PushSiteVars:($data.checkedValues -contains 'inputVar') ` -InstallRMM: ($data.checkedValues -contains 'rmm') ` -SaveCopy: ($data.checkedValues -contains 'exe') Respond-Text $Context "Triggered DattoRMM for $($data.Name)" } catch { Write-LogHybrid "Handle-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent $Context.Response.StatusCode = 500 Respond-Text $Context "Internal server error during DattoRMM install." } } function Handle-InstallChrome { param($Context) try { winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements Write-LogHybrid "Installed Google Chrome via winget" Success SVSApps -LogToEvent Respond-Text $Context "Chrome installed" } catch { Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } function Handle-InstallAcrobat { param($Context) try { winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements Write-LogHybrid "Installed Adobe Acrobat Reader (64-bit) via winget" Success SVSApps -LogToEvent Respond-Text $Context "Acrobat Reader installed" } catch { Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } function Handle-UninstallCyberQP { param($Context) try { if (Get-Command Uninstall-CyberQP -ErrorAction Stop) { Uninstall-CyberQP Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent Respond-Text $Context "CyberQP uninstalled." } else { throw "Uninstall-CyberQP cmdlet not found in SVSMSP toolkit." } } catch { Write-LogHybrid "Error uninstalling CyberQP: $($_.Exception.Message)" Error OffBoard -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } function Handle-UninstallSVSHelpDesk { param($Context) try { if (Get-Command Uninstall-SVSHelpDesk -ErrorAction Stop) { Uninstall-SVSHelpDesk Write-LogHybrid "SVS HelpDesk uninstalled" Success OffBoard -LogToEvent Respond-Text $Context "SVS HelpDesk uninstalled." } else { throw "Uninstall-SVSHelpDesk cmdlet not found in SVSMSP toolkit." } } catch { Write-LogHybrid "Error uninstalling SVS HelpDesk: $($_.Exception.Message)" Error OffBoard -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } function Handle-UninstallThreatLocker { param($Context) try { if (Get-Command Uninstall-ThreatLocker -ErrorAction Stop) { Uninstall-ThreatLocker Write-LogHybrid "ThreatLocker uninstalled" Success OffBoard -LogToEvent Respond-Text $Context "ThreatLocker uninstalled." } else { throw "Uninstall-ThreatLocker cmdlet not found in SVSMSP toolkit." } } catch { Write-LogHybrid "Error uninstalling ThreatLocker: $($_.Exception.Message)" Error OffBoard -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } function Handle-UninstallRocketCyber { param($Context) try { if (Get-Command Uninstall-RocketCyber -ErrorAction Stop) { Uninstall-RocketCyber Write-LogHybrid "RocketCyber uninstalled" Success OffBoard -LogToEvent Respond-Text $Context "RocketCyber uninstalled." } else { throw "Uninstall-RocketCyber cmdlet not found in SVSMSP toolkit." } } catch { Write-LogHybrid "Error uninstalling RocketCyber: $($_.Exception.Message)" Error OffBoard -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } function Handle-CleanupSVSMSP { param($Context) try { if (Get-Command Install-SVSMSP -ErrorAction Stop) { Install-SVSMSP -Cleanup Write-LogHybrid "SVSMSP toolkit cleanup complete (module, repo, registry)." Success OffBoard -LogToEvent Respond-Text $Context "SVSMSP toolkit cleanup complete." } else { throw "Install-SVSMSP function not found in current session." } } catch { Write-LogHybrid "Error cleaning up SVSMSP toolkit: $($_.Exception.Message)" Error OffBoard -LogToEvent Respond-Text $Context "ERROR: $($_.Exception.Message)" } } # ===================================================================== # INSTALL-DattoRMM # ===================================================================== function Install-DattoRMM { [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] param ( [switch]$UseWebhook, [string]$WebhookPassword, [string]$WebhookUrl = $Global:DattoWebhookUrl, [string]$ApiUrl, [string]$ApiKey, [string]$ApiSecretKey, [switch]$FetchSites, [switch]$SaveSitesList, [string]$OutputFile = 'datto_sites.csv', [switch]$PushSiteVars, [switch]$InstallRMM, [switch]$SaveCopy, [string]$SiteUID, [string]$SiteName ) if ($SaveSitesList -and -not $FetchSites) { Write-LogHybrid "-SaveSitesList requires -FetchSites." Error DattoRMM -LogToEvent return } if ($UseWebhook) { if (-not $WebhookPassword) { Write-LogHybrid "Webhook password missing." Error DattoRMM -LogToEvent return } try { $resp = Invoke-RestMethod -Uri $WebhookUrl ` -Headers @{ SVSMSPKit = $WebhookPassword } ` -Method GET $ApiUrl = $resp.ApiUrl $ApiKey = $resp.ApiKey $ApiSecretKey = $resp.ApiSecretKey Write-LogHybrid "Webhook credentials fetched." Success DattoRMM -LogToEvent } catch { Write-LogHybrid "Failed to fetch webhook credentials: $($_.Exception.Message)" Error DattoRMM -LogToEvent return } } if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) { Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent return } [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 try { $publicCred = New-Object System.Management.Automation.PSCredential( 'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force) ) $tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" ` -Credential $publicCred ` -Method Post ` -ContentType 'application/x-www-form-urlencoded' ` -Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey" $token = $tokenResp.access_token Write-LogHybrid "OAuth token acquired." Success DattoRMM -LogToEvent } catch { Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent return } $headers = @{ Authorization = "Bearer $token" } if ($FetchSites) { try { $sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers $siteList = $sitesResp.sites | ForEach-Object { [PSCustomObject]@{ Name = $_.name; UID = $_.uid } } Write-LogHybrid "Fetched $($siteList.Count) sites." Success DattoRMM -LogToEvent if ($SaveSitesList) { $desktop = [Environment]::GetFolderPath('Desktop') $path = Join-Path $desktop $OutputFile $ext = [IO.Path]::GetExtension($OutputFile).ToLower() if ($ext -eq '.json') { $siteList | ConvertTo-Json -Depth 3 | Out-File -FilePath $path -Encoding UTF8 } else { $siteList | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8 } Write-LogHybrid "Wrote $($siteList.Count) sites to $path" Success DattoRMM -LogToEvent } return $siteList } catch { Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM -LogToEvent return @() } } if ($PushSiteVars) { try { $varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM -LogToEvent } catch { Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent } $regPath = "HKLM:\Software\SVS\Deployment" foreach ($v in $varsResp.variables) { try { if (-not (Test-Path $regPath)) { New-Item -Path $regPath -Force | Out-Null } New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM -LogToEvent } catch { Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent } } } if ($InstallRMM) { if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) { try { $dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID" $tmp = "$env:TEMP\AgentInstall.exe" Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM -LogToEvent Start-Process -FilePath $tmp -NoNewWindow Write-LogHybrid "RMM agent installer launched." Success DattoRMM -LogToEvent } catch { Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent } } } if ($SaveCopy) { 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 Respond-Text $Context "Server shutting down." $Global:Listener.Stop() return } if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') { Handle-FetchSites $Context return } if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) { $page = if ($path -eq '') { 'onboard' } else { $path } $html = Get-UIHtml -Page $page Respond-HTML $Context $html return } $task = $Global:SamyTasks | Where-Object Name -EQ $path if ($task) { & $task.HandlerFn $Context return } $Context.Response.StatusCode = 404 Respond-Text $Context '404 - Not Found' } # ===================================================================== # MAIN ENTRY LOGIC (PARAMETER SET SWITCH) # ===================================================================== switch ($PSCmdlet.ParameterSetName) { 'Toolkit' { Write-LogHybrid "Toolkit-only mode" Info Startup -LogToEvent Install-SVSMSP -InstallToolkit return } 'Cleanup' { Write-LogHybrid "Running Toolkit cleanup mode" Info Startup -LogToEvent Install-SVSMSP -Cleanup return } 'DattoFetch' { Write-LogHybrid "Fetching site list only..." Info DattoAuth -LogToEvent $sites = Install-DattoRMM ` -UseWebhook ` -WebhookPassword $WebhookPassword ` -FetchSites ` -SaveSitesList:$SaveSitesList ` -OutputFile $OutputFile Write-LogHybrid "Done." Success DattoAuth -LogToEvent return } 'DattoInstall' { Write-LogHybrid "Headless DattoRMM deploy" Info DattoAuth -LogToEvent if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless install")) { Install-DattoRMM ` -UseWebhook ` -WebhookPassword $WebhookPassword ` -SiteUID $SiteUID ` -SiteName $SiteName ` -PushSiteVars:$PushSiteVars ` -InstallRMM:$InstallRMM ` -SaveCopy:$SaveCopy } return } 'UI' { $url = "http://localhost:$Port/" Write-LogHybrid "Starting ScriptMonkey UI on $url" Info Startup $edgeCandidates = @( "${env:ProgramFiles(x86)}\Microsoft\Edge\Application\msedge.exe", "$env:ProgramFiles\Microsoft\Edge\Application\msedge.exe" ) $edgePath = $edgeCandidates | Where-Object { $_ -and (Test-Path $_) } | Select-Object -First 1 if (-not $edgePath) { $cmd = Get-Command -Name 'msedge.exe' -ErrorAction SilentlyContinue if ($cmd) { $edgePath = $cmd.Path } } Start-Job -Name 'OpenScriptMonkeyUI' -ScriptBlock { param([string]$u, [string]$edge) Start-Sleep -Milliseconds 400 try { if ($edge -and (Test-Path $edge)) { Start-Process -FilePath $edge -ArgumentList @('--new-window', "--app=$u") } else { Start-Process -FilePath $u } } catch { } } -ArgumentList $url, $edgePath | Out-Null Start-Server return } } } # ===================================================================== # ENTRYPOINT WRAPPER # ===================================================================== if ($MyInvocation.InvocationName -eq '.') { # dot-sourced, do not auto-run } elseif ($PSCommandPath) { Invoke-ScriptMonkey @PSBoundParameters } else { if ($args.Count -gt 0) { $namedArgs = @{} for ($i = 0; $i -lt $args.Count; $i++) { if ($args[$i] -is [string] -and $args[$i].StartsWith('-')) { $key = $args[$i].TrimStart('-') $next = $args[$i + 1] if ($next -and ($next -notlike '-*')) { $namedArgs[$key] = $next $i++ } else { $namedArgs[$key] = $true } } } Invoke-ScriptMonkey @namedArgs } else { Invoke-ScriptMonkey } }