From f2352a44a88ec66ce9640c4b0ab8da44bcad84ec Mon Sep 17 00:00:00 2001 From: Stephan Yelle Date: Tue, 9 Dec 2025 22:56:33 -0500 Subject: [PATCH] Add test/samy.ps1 --- test/samy.ps1 | 352 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 352 insertions(+) create mode 100644 test/samy.ps1 diff --git a/test/samy.ps1 b/test/samy.ps1 new file mode 100644 index 0000000..de2a5e3 --- /dev/null +++ b/test/samy.ps1 @@ -0,0 +1,352 @@ +<# +.SYNOPSIS + Script Automation Monkey (SAMY) main entry point. + +.DESCRIPTION + This file is now the orchestration layer only. It handles: + - Execution policy bypass for restricted environments + - Global config (branch, repo base, URLs) + - Loading subsystem scripts (logging, SVSMSP, Datto, printers, UI, HTTP, etc.) + - Exposing Invoke-ScriptAutomationMonkey with parameter sets for: + - UI + - Toolkit-only install + - Toolkit cleanup + - Headless Datto site fetch + - Headless Datto install + - Headless offboarding + - The iwr | iex glue at the bottom so remote calls still work. + + All heavy logic lives in the Samy.*.ps1 subsystem files that are dot-sourced or + loaded from your Git repo. +#> + +#region 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://samy.svstools.com' -UseBasicParsing | iex }" + } + + exit +} + +#endregion Safely bypass Restricted Execution Policy + +#region Global defaults and config + +[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 +$ProgressPreference = 'SilentlyContinue' +$ConfirmPreference = 'None' + +# Default HTTP listening port for UI +$Script:SamyPort = 8082 + +# SAMY asset config (change branch or base once and it updates everything) +$Script:SamyBranch = 'beta' # or 'main' +$Script:SamyRepoBase = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch' + +# Top level assets +$Script:SamyTopLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_logo.svg" +$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" + +# Datto webhook URL (used by Datto subsystem) +$Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' + +# In-memory log cache +if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() +} + +#endregion Global defaults and config + +#region Module loader + +function Import-SamyModule { + <# + .SYNOPSIS + Loads a SAMY subsystem script from local disk or from the Git repo. + + .DESCRIPTION + 1. If running from a saved script (PSCommandPath) and the file exists next to it, + dot-sources that local file (dev mode). + 2. Otherwise, downloads the module from $Script:SamyRepoBase / $Script:SamyBranch + and Invoke-Expression on its content (remote iwr|iex mode). + + .PARAMETER Name + File name of the module, for example "Samy.Logging.ps1". + #> + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Name + ) + + # 1) Local dev mode: script saved to disk, use PSScriptRoot + if ($PSCommandPath) { + $localPath = Join-Path -Path $PSScriptRoot -ChildPath $Name + if (Test-Path $localPath) { + . $localPath + return + } + } + + # 2) Remote mode (iwr | iex): pull module from repo + $url = "$Script:SamyRepoBase/$Script:SamyBranch/$Name?raw=1" + + try { + $resp = Invoke-WebRequest -Uri $url -UseBasicParsing -ErrorAction Stop + $content = $resp.Content + if (-not $content) { + Write-Host "[Error] Module $Name from $url returned empty content." -ForegroundColor Red + throw "Empty module content." + } + Invoke-Expression $content + } + catch { + Write-Host "[Error] Failed to load module $Name from $url: $($_.Exception.Message)" -ForegroundColor Red + throw + } +} + +# Load subsystems in a predictable order +Import-SamyModule -Name 'Samy.Logging.ps1' # Write-LogHelper, Write-LogHybrid +Import-SamyModule -Name 'Samy.SVSBootstrap.ps1' # Install-SVSMSP, cleanup, NuGet bootstrap +Import-SamyModule -Name 'Samy.UI.ps1' # $Global:SamyTasks, UI HTML, Get-UIHtml, etc. +Import-SamyModule -Name 'Samy.Datto.ps1' # Install-DattoRMM, Datto HTTP handlers +Import-SamyModule -Name 'Samy.Printers.ps1' # Printer config, drivers, HTTP handlers +Import-SamyModule -Name 'Samy.Apps.ps1' # Winget app handlers (Chrome, Acrobat, etc.) +Import-SamyModule -Name 'Samy.Offboard.ps1' # Offboarding handlers and full offboard flow +Import-SamyModule -Name 'Samy.Onboarding.ps1' # Onboarding handlers, RenameComputer, etc. +Import-SamyModule -Name 'Samy.Http.ps1' # Send-Text/JSON, Dispatch-Request, Start-SamyHttpServer + +#endregion Module loader + +#region Simple helpers that are local to this main file + +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 +} + +#endregion Simple helpers + +#region Main entry point: Invoke-ScriptAutomationMonkey + +function Invoke-ScriptAutomationMonkey { + [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 params + [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 + ) + + switch ($PSCmdlet.ParameterSetName) { + + 'Toolkit' { + Write-LogHybrid "Toolkit-only mode requested." Info Startup -LogToEvent + Install-SVSMSP -InstallToolkit + return + } + + 'Cleanup' { + Write-LogHybrid "Toolkit cleanup requested." Info Startup -LogToEvent + Install-SVSMSP -Cleanup + return + } + + 'DattoFetch' { + Write-LogHybrid "DattoFetch mode: fetching site list." Info DattoAuth -LogToEvent + + $sites = Install-DattoRMM ` + -UseWebhook ` + -WebhookPassword $WebhookPassword ` + -WebhookUrl $WebhookUrl ` + -FetchSites ` + -SaveSitesList:$SaveSitesList ` + -OutputFile $OutputFile + + $count = if ($sites) { $sites.Count } else { 0 } + Write-LogHybrid "DattoFetch completed with $count sites." Success DattoAuth -LogToEvent + return + } + + 'DattoInstall' { + Write-LogHybrid "DattoInstall mode: headless RMM deploy to '$SiteName'." Info DattoAuth -LogToEvent + + if ($PSCmdlet.ShouldProcess("Datto site '$SiteName'", "Headless Install-DattoRMM")) { + Install-DattoRMM ` + -UseWebhook ` + -WebhookPassword $WebhookPassword ` + -WebhookUrl $WebhookUrl ` + -SiteUID $SiteUID ` + -SiteName $SiteName ` + -PushSiteVars:$PushSiteVars ` + -InstallRMM:$InstallRMM ` + -SaveCopy:$SaveCopy + } + + Write-LogHybrid "DattoInstall completed for '$SiteName'." Success DattoAuth -LogToEvent + return + } + + 'Offboard' { + Write-LogHybrid "Headless offboarding requested." Info OffBoard -LogToEvent + Invoke-SamyFullOffboard + return + } + + 'UI' { + # Default UI mode: launch browser and start HTTP listener + $port = $Script:SamyPort + $url = "http://localhost:$port/" + + Write-LogHybrid "Starting ScriptAutomationMonkey UI on $url" Info Startup -LogToEvent + + # Resolve Edge path explicitly + $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 } + } + + # Launch Edge (app mode) or default browser in a background job + Start-Job -Name 'OpenScriptAutomationMonkeyUI' -ScriptBlock { + param([string]$u, [string]$edgeExe) + Start-Sleep -Milliseconds 400 + try { + if ($edgeExe -and (Test-Path $edgeExe)) { + Start-Process -FilePath $edgeExe -ArgumentList @('--new-window', "--app=$u") + } + else { + Start-Process -FilePath $u + } + } + catch { } + } -ArgumentList $url, $edgePath | Out-Null + + # Start HTTP listener loop (implemented in Samy.Http.ps1) + # Expected exported function: + # Start-SamyHttpServer -Port + Start-SamyHttpServer -Port $port + return + } + } +} + +#endregion Main entry point: Invoke-ScriptAutomationMonkey + +#region Auto invoke for direct execution and iwr | iex + +if ($MyInvocation.InvocationName -eq '.') { + # Dot-sourced, just expose functions +} +elseif ($PSCommandPath) { + # Script was saved and run directly + Invoke-ScriptAutomationMonkey @PSBoundParameters +} +else { + # iwr | iex fallback with simple -Param value parsing + if ($args.Count -gt 0) { + $namedArgs = @{} + for ($i = 0; $i -lt $args.Count; $i++) { + $current = $args[$i] + if ($current -is [string] -and $current.StartsWith('-')) { + $key = $current.TrimStart('-') + $next = $null + if ($i + 1 -lt $args.Count) { + $next = $args[$i + 1] + } + + if ($next -and ($next -notlike '-*')) { + $namedArgs[$key] = $next + $i++ + } + else { + $namedArgs[$key] = $true + } + } + } + + Invoke-ScriptAutomationMonkey @namedArgs + } + else { + Invoke-ScriptAutomationMonkey + } +} + +#endregion Auto invoke for direct execution and iwr | iex