397 lines
13 KiB
PowerShell
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
|