Add test/samy.ps1

This commit is contained in:
2025-12-09 22:56:33 -05:00
parent e210bc9ad8
commit f2352a44a8

352
test/samy.ps1 Normal file
View File

@@ -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 <int>
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