Files
SAMY/samy-mini.ps1
2025-12-09 23:29:32 -05:00

397 lines
13 KiB
PowerShell

<#
.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, now under a "module" subfolder.
#>
#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
Local:
- Prefer .\module\<Name>
- Fallback to .\<Name>
Remote (iwr | iex):
- Try .../module/<Name> first
- If that 404s, fallback to .../<Name>
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)][string]$Name
)
# 1) Local dev mode: script saved to disk
if ($PSCommandPath) {
$moduleRoot = Join-Path -Path $PSScriptRoot -ChildPath 'module'
$localModulePath = Join-Path -Path $moduleRoot -ChildPath $Name
$localRootPath = Join-Path -Path $PSScriptRoot -ChildPath $Name
if (Test-Path $localModulePath) {
. $localModulePath
return
}
if (Test-Path $localRootPath) {
. $localRootPath
return
}
}
# 2) Remote mode (iwr | iex): pull module from repo
$base = "{0}/{1}" -f $Script:SamyRepoBase, $Script:SamyBranch
$primaryUrl = "{0}/module/{1}" -f $base, $Name
$fallbackUrl = "{0}/{1}" -f $base, $Name
function Invoke-LoadUrl {
param(
[Parameter(Mandatory)][string]$Url,
[Parameter(Mandatory)][string]$ModuleName
)
$resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop
$content = $resp.Content
if (-not $content) {
Write-Host ("[Error] Module {0} from {1} returned empty content." -f $ModuleName, $Url) -ForegroundColor Red
throw "Empty module content."
}
Invoke-Expression $content
}
try {
# Try /module/<Name> first
Invoke-LoadUrl -Url $primaryUrl -ModuleName $Name
}
catch [System.Net.WebException] {
$response = $_.Exception.Response
$statusCode = $null
if ($response -and $response.StatusCode) {
$statusCode = [int]$response.StatusCode
}
if ($statusCode -eq 404) {
Write-Host ("[Info] Module {0} not found at {1} (404). Trying fallback {2}." -f $Name, $primaryUrl, $fallbackUrl) -ForegroundColor Yellow
try {
Invoke-LoadUrl -Url $fallbackUrl -ModuleName $Name
}
catch {
Write-Host ("[Error] Failed to load module {0} from fallback {1}: {2}" -f $Name, $fallbackUrl, $_.Exception.Message) -ForegroundColor Red
throw
}
}
else {
Write-Host ("[Error] Failed to load module {0} from {1}: {2}" -f $Name, $primaryUrl, $_.Exception.Message) -ForegroundColor Red
throw
}
}
catch {
if (-not ($_ -is [System.Net.WebException])) {
Write-Host ("[Error] Failed to load module {0}: {1}" -f $Name, $_.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)
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