From 8e854f0ed7e73e394f1d1fb57679dce6ee9fcb4d Mon Sep 17 00:00:00 2001 From: Stephan Yelle Date: Wed, 15 Oct 2025 17:05:35 -0400 Subject: [PATCH] Add Stackmonkey.ps1 --- Stackmonkey.ps1 | 974 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 974 insertions(+) create mode 100644 Stackmonkey.ps1 diff --git a/Stackmonkey.ps1 b/Stackmonkey.ps1 new file mode 100644 index 0000000..6b65fbb --- /dev/null +++ b/Stackmonkey.ps1 @@ -0,0 +1,974 @@ +#region ScriptMonkey.ps1 (full updated script) + +#region changes to be done (now implemented) +# - Event logging uses custom Windows log "SVS Scripting". +# - Added SVS APPs tasks: +# winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements +# winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements +#endregion + +<# +.SYNOPSIS + ScriptMonkey - MSP client onboarding/offboarding toolkit with a user interface, + and optional silent install of the SVSMSP toolkit and headless DattoRMM deployment. + +.DESCRIPTION + Install-DattoRMM is a single, unified toolkit for Datto RMM operations. It can be used + interactively or via HTTP endpoints, and includes built-in validation and error trapping. + + Key features: + - Credential retrieval - securely fetches ApiUrl, ApiKey, and ApiSecretKey from a webhook. + - OAuth management - automatically acquires and refreshes bearer tokens over TLS. + - Site list fetching - returns the list of RMM sites; validates OutputFile to .csv or .json. + - Site list saving - writes fetched site list to the user's Desktop as CSV or JSON. + - Registry variable push - writes site-specific variables under HKLM:\Software\SVS\Deployment. + - Agent download & install - downloads the Datto RMM agent installer and launches it. + - Installer archiving - saves a copy of the downloaded installer to C:\Temp. + - HTTP endpoints - exposes /getpw and /installDattoRMM handlers, each wrapped in try/catch + to log errors and return proper HTTP 500 responses on failure. + - Idempotent & WhatIf support - uses ShouldProcess for safe, testable agent installs. + + Throughout, secrets are never written to logs or console, and all operations produce + clear success/failure messages via Write-LogHybrid. +#> + +#region Safely bypass Restricted Execution Policy + Elevation +# ─── Relaunch elevated (Admin) + with ExecutionPolicy Bypass when needed ─── +$needBypass = ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or (Get-ExecutionPolicy) -eq 'Restricted') +$identity = [Security.Principal.WindowsIdentity]::GetCurrent() +$principal = [Security.Principal.WindowsPrincipal]$identity +$needAdmin = -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) + +if ($needBypass -or $needAdmin) { + Write-Host "[Info] Relaunching elevated (Admin) with ExecutionPolicy Bypass..." -ForegroundColor Yellow + $args = if ($PSCommandPath) { "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" } else { "-NoProfile -ExecutionPolicy Bypass -Command `"& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }`"" } + Start-Process -FilePath "powershell.exe" -ArgumentList $args -Verb RunAs | Out-Null + exit +} + +# ─── TLS and silent defaults ─── +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ProgressPreference = 'SilentlyContinue' +$ConfirmPreference = 'None' +#endregion Safely bypass Restricted Execution Policy + Elevation + +function Invoke-ScriptMonkey { + + # ───────────────────────────────────────────────────────────────────────── + # PARAMETERS + GLOBAL VARIABLES + # ───────────────────────────────────────────────────────────────────────── + + [CmdletBinding( + DefaultParameterSetName='UI', + SupportsShouldProcess=$true, + ConfirmImpact= 'Medium' + )] + param( + # ───────────────────────────────────────────────────────── + # Toolkit-only mode + [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, + + # ───────────────────────────────────────────────────────── + # remove Toolkit + [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, + + # ───────────────────────────────────────────────────────── + # Datto headless mode + + # ─── DattoFetch & DattoInstall share the webhook creds ───────────── + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [switch]$UseWebhook, + + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [string]$WebhookPassword, + + [string]$WebhookUrl = $Global:DattoWebhookUrl, + + # ─── only DattoFetch uses these ──────────────────────────────────── + [Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites, + [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList, + [Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv', + + # ─── only DattoInstall uses these ───────────────────────────────── + [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteUID, + [Parameter(Mandatory,ParameterSetName='DattoInstall')][string] $SiteName, + [Parameter(ParameterSetName='DattoInstall')][switch] $PushSiteVars, + [Parameter(ParameterSetName='DattoInstall')][switch] $InstallRMM, + [Parameter(ParameterSetName='DattoInstall')][switch] $SaveCopy + ) + + #region global variables + + # Listening port for HTTP UI + $Port = 8082 + + # Configurable endpoints + $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' + + # Initialize a global in-memory log cache + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + #endregion global variables + + #region SVS Module + + function 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 Perform-ToolkitInstallation { + 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; return } + if ($InstallToolkit) { Perform-ToolkitInstallation; return } + Perform-ToolkitInstallation + } + + #endregion SVS Module + + #region Write-Log + + function Write-LogHelper { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "Script Automation Monkey", + [string]$EventLog = "SVS Scripting", + [int] $CustomEventID, + [string]$LogFile, + [switch]$PassThru + ) + + $idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 } + $colMap = @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" } + $EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] } + $color = $colMap[$Level] + $fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" + + Write-Host $fmt -ForegroundColor $color + + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + $Global:LogCache.Add([pscustomobject]@{ + Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + Level = $Level + Message = $fmt + }) | Out-Null + + if ($PSBoundParameters.LogFile) { + try { + "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" | + Out-File -FilePath $LogFile -Append -Encoding UTF8 + } + catch { Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow } + } + + if ($LogToEvent) { + try { + if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { + New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop + } + } catch { + Write-Host "[Warning] Could not create event log '$EventLog' or source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow + return + } + + $entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' } + + try { + Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $entryType -EventID $EventID -Message $fmt + } catch { + Write-Host "[Warning] EventLog failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + if ($PassThru) { return $Global:LogCache[-1] } + } + + function Write-LogHybrid { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "Script Automation Monkey", + [string]$EventLog = "SVS Scripting", + [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] + [string]$ForegroundColorOverride + ) + + $formatted = "[$Level] [$TaskCategory] $Message" + + if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { + Write-Host $formatted -ForegroundColor $ForegroundColorOverride + $invokeParams = @{ + Message = $Message + Level = $Level + TaskCategory = $TaskCategory + LogToEvent = $LogToEvent + EventSource = $EventSource + EventLog = $EventLog + } + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } else { Write-LogHelper @invokeParams } + } else { + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog + } else { + Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog + } + } + } + + #endregion Write-Log + + #region Tasks registry + + $Global:Tasks = @( + # 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='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' }, + @{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; 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' } + ) + + #endregion Tasks registry + + #region Build-Checkboxes + + function Build-Checkboxes { + param($Page, $Column) + + ( + $Global:Tasks | + Where-Object Page -EQ $Page | + Where-Object Column -EQ $Column | + 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" + } + + #endregion Build-Checkboxes + + #region Get-ModuleVersionHtml + 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
" + } + #endregion Get-ModuleVersionHtml + + #region HTTP prereqs (URL ACL + port check) + + function Ensure-HttpPrereqs { + param( + [int]$Port, + [string[]]$Prefixes + ) + + # Port already bound? + try { + $inUse = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop + } catch { $inUse = $null } + if ($inUse) { + $pids = ($inUse | Select-Object -ExpandProperty OwningProcess -Unique) -join ',' + throw "Port $Port is already in use (PID(s): $pids). Choose another port or stop the process." + } + + # Ensure URL ACLs + $current = (& netsh http show urlacl) 2>$null + foreach ($p in $Prefixes) { + $pfx = if ($p.EndsWith('/')) { $p } else { "$p/" } + if ($null -eq $current -or ($current -join "`n") -notmatch [regex]::Escape($pfx)) { + $user = "$env:USERDOMAIN\$env:USERNAME" + $args = "http add urlacl url=$pfx user=""$user"" listen=yes" + $psi = New-Object System.Diagnostics.ProcessStartInfo -Property @{ + FileName = 'netsh.exe' + Arguments = $args + Verb = 'runas' + UseShellExecute = $true + WindowStyle = 'Hidden' + } + $proc = [System.Diagnostics.Process]::Start($psi); $proc.WaitForExit() + if ($proc.ExitCode -ne 0) { + throw "Failed to add URL ACL for $pfx (netsh exit $($proc.ExitCode))." + } else { + Write-LogHybrid "Registered URL ACL for $pfx" "Success" "Server" -LogToEvent + } + } + } + } + + #endregion HTTP prereqs + + #region Start-Server (hardened for 25H2) + + function Start-Server { + $prefixes = @("http://localhost:$Port/","http://127.0.0.1:$Port/") + try { + Ensure-HttpPrereqs -Port $Port -Prefixes $prefixes + } catch { + Write-LogHybrid "HTTP prereq check failed: $($_.Exception.Message)" "Error" "Server" -LogToEvent + throw + } + + $Global:Listener = [System.Net.HttpListener]::new() + foreach ($p in $prefixes) { $Global:Listener.Prefixes.Add($p) } + + try { + $Global:Listener.Start() + Write-LogHybrid "Listening on $($prefixes -join ', ')" "Info" "Server" -LogToEvent + } catch { + Write-LogHybrid "HttpListener failed to start: $($_.Exception.Message)" "Error" "Server" -LogToEvent + if ($_.Exception.InnerException) { + Write-LogHybrid "Inner: $($_.Exception.InnerException.Message)" "Error" "Server" -LogToEvent + } + throw + } + + try { + while ($Global:Listener.IsListening) { + $ctx = $Global:Listener.GetContext() + try { + Dispatch-Request $ctx + } catch { + Write-LogHybrid "Dispatch error: $($_.Exception.Message)" "Error" "Server" -LogToEvent + $ctx.Response.StatusCode = 500 + Respond-Text $ctx "Internal server error." + } + } + } finally { + $Global:Listener.Close() + Write-LogHybrid "Listener closed." "Info" "Server" -LogToEvent + } + } + + #endregion Start-Server + + #region UI HTML/JS + + function Get-UIHtml { + param([string]$Page = 'onboard') + +$style = @' + +'@ + +$script = @' + +'@ + +$htmlTemplate = @" + + + + + +Script Monkey + +$style + + +
+
+ SVS Logo + {{moduleVersion}} +
+
+ SAMY Logo +
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

+
+ {{offboardCheckboxes}} +
+
+ +
+

Tweaks

+
+ {{tweaksCheckboxes}} +
+
+ +
+

SVS APPs

+
+ {{appsCheckboxes}} +
+
+
+
+$script +
+ + +
+
+ + +"@ + + # Build dynamic sections + $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:Tasks | ForEach-Object { + " { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }" + } + ) -join ",`n" + + $html = $htmlTemplate + $html = $html.Replace('{{moduleVersion}}', (Get-ModuleVersionHtml)) + $html = $html.Replace('{{onboardLeftColumn}}', $onboardLeft) + $html = $html.Replace('{{onboardRightColumn}}', $onboardRight) + $html = $html.Replace('{{offboardCheckboxes}}', $offboard) + $html = $html.Replace('{{tweaksCheckboxes}}', $tweaks) + $html = $html.Replace('{{appsCheckboxes}}', $apps) + $html = $html.Replace('{{tasksJsAll}}', $tasksJsAll) + $html = $html.Replace('{{defaultPage}}', $Page) + + return $html + } + + #endregion UI HTML/JS + + #region HTTP response helpers + 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)][object]$Context,[Parameter(Mandatory)][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 + + # store for next call + $Global:WebhookPassword = $pw + + $sites = Install-DattoRMM -UseWebhook -WebhookPassword $pw -FetchSites + 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" + } + + # New: winget app handlers + 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-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." + } + } + + #endregion HTTP response helpers + handlers + + #region 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" -L_