Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d884fc8437 | |||
| ab79f78693 | |||
| 5ecb8c295e |
@@ -1,30 +0,0 @@
|
|||||||
# Samy.Apps.ps1
|
|
||||||
# Winget app handlers
|
|
||||||
|
|
||||||
function Invoke-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
|
|
||||||
Send-Text $Context "Chrome installed"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Chrome install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-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
|
|
||||||
Send-Text $Context "Acrobat Reader installed"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Acrobat install failed: $($_.Exception.Message)" Error SVSApps -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,213 +0,0 @@
|
|||||||
# Samy.Datto.ps1
|
|
||||||
# Datto RMM helper and HTTP handlers
|
|
||||||
|
|
||||||
function Invoke-FetchSites {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
$raw = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
|
|
||||||
$pw = (ConvertFrom-Json $raw).password
|
|
||||||
|
|
||||||
$Global:WebhookPassword = $pw
|
|
||||||
|
|
||||||
$sites = Install-DattoRMM `
|
|
||||||
-UseWebhook `
|
|
||||||
-WebhookPassword $pw `
|
|
||||||
-FetchSites
|
|
||||||
|
|
||||||
Send-JSON $Context $sites
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Invoke-FetchSites error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 500
|
|
||||||
Send-Text $Context "Internal server error fetching sites."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallDattoRMM {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($Context.Request.HttpMethod -ne 'POST') {
|
|
||||||
$Context.Response.StatusCode = 405
|
|
||||||
Send-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')
|
|
||||||
|
|
||||||
Send-Text $Context "Triggered DattoRMM for $($data.Name)"
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Invoke-InstallDattoRMM error: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 500
|
|
||||||
Send-Text $Context "Internal server error during DattoRMM install."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $ApiUrl -or -not $ApiKey -or -not $ApiSecretKey) {
|
|
||||||
Write-LogHybrid "Missing required API parameters." Error DattoRMM -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
||||||
try {
|
|
||||||
$publicCred = New-Object System.Management.Automation.PSCredential(
|
|
||||||
'public-client', (ConvertTo-SecureString 'public' -AsPlainText -Force)
|
|
||||||
)
|
|
||||||
$tokenResp = Invoke-RestMethod -Uri "$ApiUrl/auth/oauth/token" `
|
|
||||||
-Credential $publicCred `
|
|
||||||
-Method Post `
|
|
||||||
-ContentType 'application/x-www-form-urlencoded' `
|
|
||||||
-Body "grant_type=password&username=$ApiKey&password=$ApiSecretKey"
|
|
||||||
$token = $tokenResp.access_token
|
|
||||||
Write-LogHybrid "OAuth token acquired." Success DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "OAuth token fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$headers = @{ Authorization = "Bearer $token" }
|
|
||||||
|
|
||||||
if ($FetchSites) {
|
|
||||||
try {
|
|
||||||
$sitesResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/account/sites" -Method Get -Headers $headers
|
|
||||||
$siteList = $sitesResp.sites | Sort-Object name | ForEach-Object {
|
|
||||||
[PSCustomObject]@{ Name = $_.name; UID = $_.uid }
|
|
||||||
}
|
|
||||||
Write-LogHybrid "Fetched $($siteList.Count) sites." Success DattoRMM -LogToEvent
|
|
||||||
|
|
||||||
if ($SaveSitesList) {
|
|
||||||
$desktop = [Environment]::GetFolderPath('Desktop')
|
|
||||||
$path = Join-Path $desktop $OutputFile
|
|
||||||
$ext = [IO.Path]::GetExtension($OutputFile).ToLower()
|
|
||||||
if ($ext -eq '.json') {
|
|
||||||
$siteList | ConvertTo-Json -Depth 3 | Out-File -FilePath $path -Encoding UTF8
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$siteList | Export-Csv -Path $path -NoTypeInformation -Encoding UTF8
|
|
||||||
}
|
|
||||||
Write-LogHybrid "Wrote $($siteList.Count) sites to $path" Success DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
return $siteList
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to fetch sites: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($PushSiteVars) {
|
|
||||||
try {
|
|
||||||
$varsResp = Invoke-RestMethod -Uri "$ApiUrl/api/v2/site/$SiteUID/variables" -Method Get -Headers $headers
|
|
||||||
Write-LogHybrid "Fetched variables for '$SiteName'." Success DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Variable fetch failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
$regPath = "HKLM:\Software\SVS\Deployment"
|
|
||||||
foreach ($v in $varsResp.variables) {
|
|
||||||
try {
|
|
||||||
if (-not (Test-Path $regPath)) {
|
|
||||||
New-Item -Path $regPath -Force | Out-Null
|
|
||||||
}
|
|
||||||
New-ItemProperty -Path $regPath -Name $v.name -Value $v.value -PropertyType String -Force | Out-Null
|
|
||||||
Write-LogHybrid "Wrote '$($v.name)' to registry." Success DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to write '$($v.name)': $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($InstallRMM) {
|
|
||||||
if ($PSCmdlet.ShouldProcess("Site '$SiteName'", "Install RMM agent")) {
|
|
||||||
try {
|
|
||||||
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
|
|
||||||
$tmp = "$env:TEMP\AgentInstall.exe"
|
|
||||||
Invoke-WebRequest -Uri $dlUrl -OutFile $tmp -UseBasicParsing
|
|
||||||
Write-LogHybrid "Downloaded agent to $tmp." Info DattoRMM -LogToEvent
|
|
||||||
Start-Process -FilePath $tmp -NoNewWindow
|
|
||||||
Write-LogHybrid "RMM agent installer launched." Success DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Agent install failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($SaveCopy) {
|
|
||||||
try {
|
|
||||||
$dlUrl = "https://zinfandel.centrastage.net/csm/profile/downloadAgent/$SiteUID"
|
|
||||||
$path = "C:\Temp\AgentInstall.exe"
|
|
||||||
if (-not (Test-Path 'C:\Temp')) {
|
|
||||||
New-Item -Path 'C:\Temp' -ItemType Directory | Out-Null
|
|
||||||
}
|
|
||||||
Invoke-WebRequest -Uri $dlUrl -OutFile $path -UseBasicParsing
|
|
||||||
Write-LogHybrid "Saved installer copy to $path." Info DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Save-copy failed: $($_.Exception.Message)" Error DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not ($PushSiteVars -or $InstallRMM -or $SaveCopy)) {
|
|
||||||
Write-LogHybrid "No action specified. Use -FetchSites, -SaveSitesList, -PushSiteVars, -InstallRMM, or -SaveCopy." Warning DattoRMM -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,220 +0,0 @@
|
|||||||
# Samy.Http.ps1
|
|
||||||
# HTTP helpers, listener, and dispatcher
|
|
||||||
|
|
||||||
function Send-Text {
|
|
||||||
param($Context, $Text)
|
|
||||||
if (-not $Context -or -not $Context.Response) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$bytes = [Text.Encoding]::UTF8.GetBytes([string]$Text)
|
|
||||||
$Context.Response.ContentType = 'text/plain'
|
|
||||||
$Context.Response.ContentLength64 = $bytes.Length
|
|
||||||
$Context.Response.OutputStream.Write($bytes,0,$bytes.Length)
|
|
||||||
$Context.Response.OutputStream.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
function Send-HTML {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][object] $Context,
|
|
||||||
[Parameter(Mandatory = $true)][string] $Html
|
|
||||||
)
|
|
||||||
if (-not $Context -or -not $Context.Response) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
$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 Send-JSON {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
$Context,
|
|
||||||
$Object
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not $Context -or -not $Context.Response) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($null -eq $Object) {
|
|
||||||
Write-LogHybrid "Send-JSON called with `$null object; returning empty JSON array." Warning Printers -LogToEvent
|
|
||||||
$json = '[]'
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
try {
|
|
||||||
$json = $Object | ConvertTo-Json -Depth 5 -ErrorAction Stop
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Send-JSON serialization failed: $($_.Exception.Message); returning empty JSON array." Error Printers -LogToEvent
|
|
||||||
$json = '[]'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = [string]$json
|
|
||||||
$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()
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Send-JSON fatal error: $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
try {
|
|
||||||
$fallback = '[]'
|
|
||||||
$bytes = [Text.Encoding]::UTF8.GetBytes($fallback)
|
|
||||||
$Context.Response.ContentType = 'application/json'
|
|
||||||
$Context.Response.ContentLength64 = $bytes.Length
|
|
||||||
$Context.Response.OutputStream.Write($bytes, 0, $bytes.Length)
|
|
||||||
$Context.Response.OutputStream.Close()
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-TasksCompleted {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
Write-LogHybrid "All UI-selected tasks processed" Info UI -LogToEvent
|
|
||||||
Send-Text $Context "Tasks completion acknowledged."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-NextFreePort {
|
|
||||||
param([int]$Start)
|
|
||||||
|
|
||||||
for ($p = [Math]::Max(1024,$Start); $p -lt 65535; $p++) {
|
|
||||||
$l = [System.Net.Sockets.TcpListener]::new([Net.IPAddress]::Loopback, $p)
|
|
||||||
try {
|
|
||||||
$l.Start()
|
|
||||||
$l.Stop()
|
|
||||||
return $p
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
}
|
|
||||||
}
|
|
||||||
throw "No free TCP port available."
|
|
||||||
}
|
|
||||||
|
|
||||||
function Dispatch-Request {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
$path = $Context.Request.Url.AbsolutePath.TrimStart('/')
|
|
||||||
|
|
||||||
if ($path -eq 'quit') {
|
|
||||||
Write-LogHybrid "Shutdown requested" Info Server -LogToEvent
|
|
||||||
Send-Text $Context "Server shutting down."
|
|
||||||
$Global:Listener.Stop()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'tasksCompleted') {
|
|
||||||
Invoke-TasksCompleted $Context
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getpw') {
|
|
||||||
Invoke-FetchSites $Context
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'renameComputer') {
|
|
||||||
Invoke-RenameComputer $Context
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'getprinters') {
|
|
||||||
Invoke-GetPrinters $Context
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Context.Request.HttpMethod -eq 'POST' -and $path -eq 'installprinters') {
|
|
||||||
Invoke-InstallPrinters $Context
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($path -in @('', 'onboard', 'offboard', 'tweaks', 'SVSApps')) {
|
|
||||||
$page = if ($path -eq '') { 'onboard' } else { $path }
|
|
||||||
$html = Get-UIHtml -Page $page
|
|
||||||
Send-HTML $Context $html
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$task = $Global:SamyTasks | Where-Object Name -EQ $path
|
|
||||||
if ($task) {
|
|
||||||
& $task.HandlerFn $Context
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$Context.Response.StatusCode = 404
|
|
||||||
Send-Text $Context '404 - Not Found'
|
|
||||||
}
|
|
||||||
|
|
||||||
function Start-SamyHttpServer {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][int]$Port
|
|
||||||
)
|
|
||||||
|
|
||||||
$Global:Listener = [System.Net.HttpListener]::new()
|
|
||||||
$primaryPrefix = "http://localhost:$Port/"
|
|
||||||
$wildcardPrefix = "http://+:$Port/"
|
|
||||||
|
|
||||||
try {
|
|
||||||
$Global:Listener.Prefixes.Add($primaryPrefix)
|
|
||||||
$Global:Listener.Start()
|
|
||||||
Write-LogHybrid "Listening on $primaryPrefix" Info Server -LogToEvent
|
|
||||||
}
|
|
||||||
catch [System.Net.HttpListenerException] {
|
|
||||||
if ($_.Exception.ErrorCode -eq 5) {
|
|
||||||
Write-LogHybrid "Access denied on $primaryPrefix. Attempting URL ACL..." Warning Server -LogToEvent
|
|
||||||
try {
|
|
||||||
$user = "$env:USERDOMAIN\$env:USERNAME"
|
|
||||||
if (-not $user.Trim()) {
|
|
||||||
$user = $env:USERNAME
|
|
||||||
}
|
|
||||||
Start-Process -FilePath "netsh" -ArgumentList "http add urlacl url=$wildcardPrefix user=`"$user`" listen=yes" -Verb RunAs -WindowStyle Hidden -Wait
|
|
||||||
$Global:Listener = [System.Net.HttpListener]::new()
|
|
||||||
$Global:Listener.Prefixes.Add($wildcardPrefix)
|
|
||||||
$Global:Listener.Start()
|
|
||||||
Write-LogHybrid "Listening on $wildcardPrefix (URL ACL added for $user)" Success Server -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "URL ACL registration failed: $($_.Exception.Message)" Error Server -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
elseif ($_.Exception.NativeErrorCode -in 32,183) {
|
|
||||||
$old = $Port
|
|
||||||
$Port = Get-NextFreePort -Start ($Port + 1)
|
|
||||||
$Global:Listener = [System.Net.HttpListener]::new()
|
|
||||||
$primaryPrefix = "http://localhost:$Port/"
|
|
||||||
$Global:Listener.Prefixes.Add($primaryPrefix)
|
|
||||||
$Global:Listener.Start()
|
|
||||||
Write-LogHybrid "Port $old busy. Listening on $primaryPrefix" Warning Server -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "HttpListener start failed: $($_.Exception.Message)" Error Server -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
while ($Global:Listener.IsListening) {
|
|
||||||
$ctx = $Global:Listener.GetContext()
|
|
||||||
try {
|
|
||||||
Dispatch-Request $ctx
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
$Global:Listener.Close()
|
|
||||||
Write-LogHybrid "Listener closed." Info Server -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,269 +0,0 @@
|
|||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Core logging utilities for SAMY.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Provides:
|
|
||||||
- Write-LogHelper : standalone logger with console, file, and Event Log support.
|
|
||||||
- Write-LogHybrid : wrapper that prefers toolkit Write-Log if present, else falls back.
|
|
||||||
|
|
||||||
This module is loaded first so that other subsystems can safely call Write-LogHybrid.
|
|
||||||
#>
|
|
||||||
|
|
||||||
# Ensure global log structures exist
|
|
||||||
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
|
|
||||||
$Global:LogCache = [System.Collections.ArrayList]::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $Global:EventSourceInitState) {
|
|
||||||
$Global:EventSourceInitState = @{}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-LogHelper {
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Standardized logging utility with console/file output and Windows Event Log support.
|
|
||||||
|
|
||||||
.DESCRIPTION
|
|
||||||
Mirrors the SVSMSP toolkit Write-Log so that Write-LogHybrid can safely fall back
|
|
||||||
when the module is not loaded.
|
|
||||||
|
|
||||||
.NOTES
|
|
||||||
Default EventLog : SVSMSP Events
|
|
||||||
Default Source : SVSMSP_Module
|
|
||||||
#>
|
|
||||||
[CmdletBinding()]
|
|
||||||
param (
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Message,
|
|
||||||
|
|
||||||
[ValidateSet("Info", "Warning", "Error", "Success", "General")]
|
|
||||||
[string]$Level = "Info",
|
|
||||||
|
|
||||||
[string]$TaskCategory = "GeneralTask",
|
|
||||||
|
|
||||||
[switch]$LogToEvent = $false,
|
|
||||||
|
|
||||||
[string]$EventSource = "SAMY",
|
|
||||||
|
|
||||||
[string]$EventLog = "SVSMSP Events",
|
|
||||||
|
|
||||||
[int]$CustomEventID,
|
|
||||||
|
|
||||||
[string]$LogFile,
|
|
||||||
|
|
||||||
[switch]$PassThru
|
|
||||||
)
|
|
||||||
|
|
||||||
# Event ID and console color
|
|
||||||
$EventID = if ($CustomEventID) { $CustomEventID } else {
|
|
||||||
switch ($Level) {
|
|
||||||
"Info" { 1000 }
|
|
||||||
"Warning" { 2000 }
|
|
||||||
"Error" { 3000 }
|
|
||||||
"Success" { 4000 }
|
|
||||||
default { 1000 }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$Color = switch ($Level) {
|
|
||||||
"Info" { "Cyan" }
|
|
||||||
"Warning" { "Yellow" }
|
|
||||||
"Error" { "Red" }
|
|
||||||
"Success" { "Green" }
|
|
||||||
default { "White" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$FormattedMessage = "[{0}] [{1}] {2} (Event ID: {3})" -f $Level, $TaskCategory, $Message, $EventID
|
|
||||||
Write-Host $FormattedMessage -ForegroundColor $Color
|
|
||||||
|
|
||||||
# In-memory cache
|
|
||||||
if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) {
|
|
||||||
$Global:LogCache = [System.Collections.ArrayList]::new()
|
|
||||||
}
|
|
||||||
|
|
||||||
$logEntry = [PSCustomObject]@{
|
|
||||||
Timestamp = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
|
|
||||||
Level = $Level
|
|
||||||
Message = $FormattedMessage
|
|
||||||
}
|
|
||||||
[void]$Global:LogCache.Add($logEntry)
|
|
||||||
|
|
||||||
# Optional file output
|
|
||||||
if ($LogFile) {
|
|
||||||
try {
|
|
||||||
"{0} {1}" -f $logEntry.Timestamp, $FormattedMessage |
|
|
||||||
Out-File -FilePath $LogFile -Append -Encoding UTF8
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[Warning] Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Windows Event Log (with one-time init)
|
|
||||||
if ($LogToEvent) {
|
|
||||||
if (-not $Global:EventSourceInitState) {
|
|
||||||
$Global:EventSourceInitState = @{}
|
|
||||||
}
|
|
||||||
|
|
||||||
$EntryType = switch ($Level) {
|
|
||||||
"Info" { "Information" }
|
|
||||||
"Warning" { "Warning" }
|
|
||||||
"Error" { "Error" }
|
|
||||||
"Success" { "Information" }
|
|
||||||
default { "Information" }
|
|
||||||
}
|
|
||||||
|
|
||||||
$sourceKey = "{0}|{1}" -f $EventLog, $EventSource
|
|
||||||
|
|
||||||
if (-not $Global:EventSourceInitState.ContainsKey($sourceKey) -or
|
|
||||||
-not $Global:EventSourceInitState[$sourceKey]) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
|
|
||||||
|
|
||||||
# Check if current token is admin
|
|
||||||
$isAdmin = $false
|
|
||||||
try {
|
|
||||||
$current = [Security.Principal.WindowsIdentity]::GetCurrent()
|
|
||||||
$principal = New-Object Security.Principal.WindowsPrincipal($current)
|
|
||||||
$isAdmin = $principal.IsInRole(
|
|
||||||
[Security.Principal.WindowsBuiltInRole]::Administrator
|
|
||||||
)
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$isAdmin = $false
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($isAdmin) {
|
|
||||||
New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$helperScript = @"
|
|
||||||
if (-not [System.Diagnostics.EventLog]::SourceExists('$EventSource')) {
|
|
||||||
New-EventLog -LogName '$EventLog' -Source '$EventSource'
|
|
||||||
}
|
|
||||||
"@
|
|
||||||
|
|
||||||
$tempPath = [System.IO.Path]::Combine(
|
|
||||||
$env:TEMP,
|
|
||||||
("Init_{0}_{1}.ps1" -f $EventLog, $EventSource).Replace(' ', '_')
|
|
||||||
)
|
|
||||||
|
|
||||||
$helperScript | Set-Content -Path $tempPath -Encoding UTF8
|
|
||||||
|
|
||||||
try {
|
|
||||||
$null = Start-Process -FilePath "powershell.exe" `
|
|
||||||
-ArgumentList "-ExecutionPolicy Bypass -File `"$tempPath`"" `
|
|
||||||
-Verb RunAs -Wait -PassThru
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[Warning] Auto-elevation to create Event Log '$EventLog' / source '$EventSource' failed: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Remove-Item -Path $tempPath -ErrorAction SilentlyContinue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) {
|
|
||||||
$Global:EventSourceInitState[$sourceKey] = $true
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$Global:EventSourceInitState[$sourceKey] = $false
|
|
||||||
Write-Host "[Warning] Event source '$EventSource' does not exist and could not be created. Skipping Event Log write." -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[Warning] Failed to initialize Event Log '$EventLog' / source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
$Global:EventSourceInitState[$sourceKey] = $false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Global:EventSourceInitState[$sourceKey]) {
|
|
||||||
try {
|
|
||||||
$EventMessage = "TaskCategory: {0} | Message: {1}" -f $TaskCategory, $Message
|
|
||||||
Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $EntryType -EventId $EventID -Message $EventMessage
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-Host "[Warning] Failed to write to Event Log: $($_.Exception.Message)" -ForegroundColor Yellow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($PassThru) {
|
|
||||||
return $logEntry
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Write-LogHybrid {
|
|
||||||
<#
|
|
||||||
.SYNOPSIS
|
|
||||||
Wrapper that prefers SVSMSP Write-Log if available, else falls back to Write-LogHelper.
|
|
||||||
#>
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)]
|
|
||||||
[string]$Message,
|
|
||||||
|
|
||||||
[ValidateSet("Info", "Warning", "Error", "Success", "General")]
|
|
||||||
[string]$Level = "Info",
|
|
||||||
|
|
||||||
[string]$TaskCategory = "GeneralTask",
|
|
||||||
|
|
||||||
[switch]$LogToEvent,
|
|
||||||
|
|
||||||
[string]$EventSource = "SVSMSP_Module",
|
|
||||||
|
|
||||||
[string]$EventLog = "SVSMSP Events",
|
|
||||||
|
|
||||||
[int]$CustomEventID,
|
|
||||||
|
|
||||||
[string]$LogFile,
|
|
||||||
|
|
||||||
[switch]$PassThru,
|
|
||||||
|
|
||||||
[ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")]
|
|
||||||
[string]$ForegroundColorOverride
|
|
||||||
)
|
|
||||||
|
|
||||||
$formatted = "[{0}] [{1}] {2}" -f $Level, $TaskCategory, $Message
|
|
||||||
|
|
||||||
$invokeParams = @{
|
|
||||||
Message = $Message
|
|
||||||
Level = $Level
|
|
||||||
TaskCategory = $TaskCategory
|
|
||||||
LogToEvent = $LogToEvent
|
|
||||||
EventSource = $EventSource
|
|
||||||
EventLog = $EventLog
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($PSBoundParameters.ContainsKey('CustomEventID')) {
|
|
||||||
$invokeParams.CustomEventID = $CustomEventID
|
|
||||||
}
|
|
||||||
if ($PSBoundParameters.ContainsKey('LogFile')) {
|
|
||||||
$invokeParams.LogFile = $LogFile
|
|
||||||
}
|
|
||||||
if ($PassThru) {
|
|
||||||
$invokeParams.PassThru = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) {
|
|
||||||
Write-Host $formatted -ForegroundColor $ForegroundColorOverride
|
|
||||||
|
|
||||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
|
||||||
Write-Log @invokeParams
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHelper @invokeParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
if (Get-Command Write-Log -ErrorAction SilentlyContinue) {
|
|
||||||
Write-Log @invokeParams
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHelper @invokeParams
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
# Samy.Offboard.ps1
|
|
||||||
# Offboarding handlers and full offboard flow
|
|
||||||
|
|
||||||
function Invoke-UninstallCyberQP {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Get-Command Uninstall-CyberQP -ErrorAction Stop) {
|
|
||||||
Uninstall-CyberQP
|
|
||||||
Write-LogHybrid "CyberQP uninstalled" Success OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "CyberQP uninstalled."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "Uninstall-CyberQP cmdlet not found in SVSMSP toolkit."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Error uninstalling CyberQP: $($_.Exception.Message)" Error OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-UninstallHelpDesk {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Get-Command Uninstall-HelpDesk -ErrorAction Stop) {
|
|
||||||
Uninstall-HelpDesk
|
|
||||||
Write-LogHybrid "SVS HelpDesk uninstalled" Success OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "SVS HelpDesk uninstalled."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "Uninstall-HelpDesk cmdlet not found in SVSMSP toolkit."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Error uninstalling SVS HelpDesk: $($_.Exception.Message)" Error OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-UninstallThreatLocker {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Get-Command Uninstall-ThreatLocker -ErrorAction Stop) {
|
|
||||||
Uninstall-ThreatLocker
|
|
||||||
Write-LogHybrid "ThreatLocker uninstalled" Success OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "ThreatLocker uninstalled."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "Uninstall-ThreatLocker cmdlet not found in SVSMSP toolkit."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Error uninstalling ThreatLocker: $($_.Exception.Message)" Error OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-UninstallRocketCyber {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Get-Command Uninstall-RocketCyber -ErrorAction Stop) {
|
|
||||||
Uninstall-RocketCyber
|
|
||||||
Write-LogHybrid "RocketCyber uninstalled" Success OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "RocketCyber uninstalled."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "Uninstall-RocketCyber cmdlet not found in SVSMSP toolkit."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Error uninstalling RocketCyber: $($_.Exception.Message)" Error OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-CleanupSVSMSP {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Get-Command Install-SVSMSP -ErrorAction Stop) {
|
|
||||||
Install-SVSMSP -Cleanup
|
|
||||||
Write-LogHybrid "SVSMSP toolkit cleanup completed (module, repo, registry)." Success OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "SVSMSP toolkit cleanup completed."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw "Install-SVSMSP function not found in current session."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Error cleaning up SVSMSP toolkit: $($_.Exception.Message)" Error OffBoard -LogToEvent
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-SamyFullOffboard {
|
|
||||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
||||||
param()
|
|
||||||
|
|
||||||
$offboardTasks = $Global:SamyTasks | Where-Object Page -eq 'offboard'
|
|
||||||
|
|
||||||
if (-not $offboardTasks) {
|
|
||||||
Write-LogHybrid "No offboard tasks configured." Warning OffBoard -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $PSCmdlet.ShouldProcess("Full off-boarding flow", "Execute every offboard task")) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($task in $offboardTasks) {
|
|
||||||
try {
|
|
||||||
Write-LogHybrid "Running offboard task: $($task.Label)" Info OffBoard -LogToEvent
|
|
||||||
|
|
||||||
if (-not (Get-Command $task.HandlerFn -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-LogHybrid "Missing handler $($task.HandlerFn)" Error OffBoard -LogToEvent
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
& $task.HandlerFn $null
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Offboard task $($task.Label) failed: $($_.Exception.Message)" Error OffBoard -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Headless offboarding completed" Success OffBoard -LogToEvent
|
|
||||||
}
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
# Samy.Onboarding.ps1
|
|
||||||
# Onboarding HTTP handlers and rename computer
|
|
||||||
|
|
||||||
function Invoke-SetSVSPowerPlan {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
Set-SVSPowerPlan
|
|
||||||
|
|
||||||
Write-LogHybrid "PowerPlan set" Success OnBoard
|
|
||||||
Send-Text $Context "PowerPlan applied"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallSVSMSP {
|
|
||||||
param($Context)
|
|
||||||
Write-LogHybrid "HTTP trigger: Invoke-InstallSVSMSP" Info OnBoard
|
|
||||||
try {
|
|
||||||
Install-SVSMSP -InstallToolkit
|
|
||||||
Send-Text $Context "SVSMSP Module installed/updated."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Error in Install-SVSMSP: $_" Error OnBoard
|
|
||||||
Send-Text $Context "ERROR: $_"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallCyberQP {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
Install-CyberQP
|
|
||||||
|
|
||||||
Write-LogHybrid "CyberQP installed" Success OnBoard
|
|
||||||
Send-Text $Context "CyberQP installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallThreatLocker {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
Install-ThreatLocker
|
|
||||||
|
|
||||||
Write-LogHybrid "ThreatLocker installed" Success OnBoard
|
|
||||||
Send-Text $Context "ThreatLocker installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallRocketCyber {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
Install-RocketCyber
|
|
||||||
|
|
||||||
Write-LogHybrid "RocketCyber installed" Success OnBoard
|
|
||||||
Send-Text $Context "RocketCyber installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallHelpDesk {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
Install-svsHelpDesk
|
|
||||||
|
|
||||||
Write-LogHybrid "SVS HelpDesk installed" Success OnBoard
|
|
||||||
Send-Text $Context "SVS HelpDesk installed"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-SetEdgeDefaultSearchEngine {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-LogHybrid "Configuring Edge default search provider" Info OnBoard
|
|
||||||
set-EdgeDefaultSearchEngine
|
|
||||||
Write-LogHybrid "Edge default search set to Google" Success OnBoard
|
|
||||||
Send-Text $Context "Edge default search provider configured."
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to set Edge default search: $($_.Exception.Message)" Error OnBoard
|
|
||||||
Send-Text $Context "ERROR: $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-RenameComputer {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($Context.Request.HttpMethod -ne 'POST') {
|
|
||||||
$Context.Response.StatusCode = 405
|
|
||||||
Send-Text $Context 'Use POST'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
|
|
||||||
if (-not $rawBody) {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Missing request body.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$body = $rawBody | ConvertFrom-Json
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Invalid JSON body.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$newName = $body.newName
|
|
||||||
|
|
||||||
if (-not (Test-ComputerName -Name $newName)) {
|
|
||||||
Write-LogHybrid "RenameComputer: invalid computer name '$newName'." Error OnBoard -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-JSON $Context @{
|
|
||||||
Success = $false
|
|
||||||
Error = "Invalid computer name. Must be 1-15 characters and use only letters, numbers, and hyphens."
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "RenameComputer: renaming computer to '$newName'." Info OnBoard -LogToEvent
|
|
||||||
|
|
||||||
try {
|
|
||||||
Rename-Computer -NewName $newName -Force -ErrorAction Stop
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "RenameComputer: rename failed: $($_.Exception.Message)" Error OnBoard -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 500
|
|
||||||
Send-JSON $Context @{
|
|
||||||
Success = $false
|
|
||||||
Error = $_.Exception.Message
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "RenameComputer: rename complete, reboot required for new name to apply." Success OnBoard -LogToEvent
|
|
||||||
|
|
||||||
Send-JSON $Context @{
|
|
||||||
Success = $true
|
|
||||||
NewName = $newName
|
|
||||||
Note = "Rename successful. A reboot is required for the new name to take effect."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Invoke-RenameComputer fatal error: $($_.Exception.Message)" Error OnBoard -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 500
|
|
||||||
Send-Text $Context "Internal error during computer rename."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,649 +0,0 @@
|
|||||||
# Samy.Printers.ps1
|
|
||||||
# Printer config, driver, and HTTP handlers
|
|
||||||
|
|
||||||
function Get-SamyDriverRootFolder {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param()
|
|
||||||
|
|
||||||
$root = Join-Path $env:ProgramData 'SVS\Samy\Drivers'
|
|
||||||
|
|
||||||
if (-not (Test-Path $root)) {
|
|
||||||
try {
|
|
||||||
New-Item -Path $root -ItemType Directory -Force | Out-Null
|
|
||||||
Write-LogHybrid "Created driver root folder '$root'." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to create driver root folder '$root': $($_.Exception.Message)" Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $root
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SamyDriverFolderForProfile {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][pscustomobject]$Profile
|
|
||||||
)
|
|
||||||
|
|
||||||
$root = Get-SamyDriverRootFolder
|
|
||||||
|
|
||||||
if ($Profile.PSObject.Properties.Name -contains 'DriverFolderName' -and $Profile.DriverFolderName) {
|
|
||||||
$folderName = $Profile.DriverFolderName
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
$folderName = "$($Profile.ClientCode)_$($Profile.ProfileName)"
|
|
||||||
}
|
|
||||||
|
|
||||||
$dest = Join-Path $root $folderName
|
|
||||||
|
|
||||||
if (-not (Test-Path $dest)) {
|
|
||||||
try {
|
|
||||||
New-Item -Path $dest -ItemType Directory -Force | Out-Null
|
|
||||||
Write-LogHybrid "Created driver folder '$dest'." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to create driver folder '$dest': $($_.Exception.Message)" Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $dest
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SamyDriverPackageUrl {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][pscustomobject]$Profile
|
|
||||||
)
|
|
||||||
|
|
||||||
if ($Profile.PSObject.Properties.Name -contains 'DriverPackageUrl' -and $Profile.DriverPackageUrl) {
|
|
||||||
return $Profile.DriverPackageUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
|
|
||||||
return "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
|
|
||||||
}
|
|
||||||
|
|
||||||
return $null
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SamyClientListFromServer {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Uri,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$Password
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
Write-LogHybrid "Calling client list service at $Uri" Info Printers -LogToEvent
|
|
||||||
|
|
||||||
$headers = @{
|
|
||||||
SAMYPW = $Password
|
|
||||||
}
|
|
||||||
|
|
||||||
$resp = Invoke-RestMethod -Uri $Uri `
|
|
||||||
-Method Post `
|
|
||||||
-Headers $headers `
|
|
||||||
-ContentType 'application/json' `
|
|
||||||
-ErrorAction Stop
|
|
||||||
|
|
||||||
if (-not $resp) {
|
|
||||||
Write-LogHybrid "Client list service returned no data." Warning Printers -LogToEvent
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($resp -is [System.Collections.IEnumerable] -and -not ($resp -is [string])) {
|
|
||||||
return @($resp)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return ,$resp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid ("Get-SamyClientListFromServer failed for {0}: {1}" -f $Uri, $_.Exception.Message) Error Printers -LogToEvent
|
|
||||||
return @()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-GetPrinters {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($Context.Request.HttpMethod -ne 'POST') {
|
|
||||||
$Context.Response.StatusCode = 405
|
|
||||||
Send-Text $Context 'Use POST'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
|
|
||||||
if (-not $rawBody) {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Missing request body.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$body = $rawBody | ConvertFrom-Json
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Invalid JSON body.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$password = $body.password
|
|
||||||
if (-not $password) {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Password is required.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$uri = 'https://bananas.svstools.ca/getprinters'
|
|
||||||
Write-LogHybrid "Fetching printers from $uri" Info Printers -LogToEvent
|
|
||||||
|
|
||||||
$printers = Get-SamyClientListFromServer -Uri $uri -Password $password
|
|
||||||
|
|
||||||
if ($null -eq $printers) {
|
|
||||||
Write-LogHybrid "Get-SamyClientListFromServer returned `$null; sending empty JSON array." Warning Printers -LogToEvent
|
|
||||||
$printers = @()
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Update-SamyPrinterConfig -PrinterProfiles $printers -SkipIfEmpty
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Update-SamyPrinterConfig failed: $($_.Exception.Message)" Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
Send-JSON $Context $printers
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Invoke-GetPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 500
|
|
||||||
Send-Text $Context "Internal server error fetching printers."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-InstallPrinters {
|
|
||||||
param($Context)
|
|
||||||
|
|
||||||
try {
|
|
||||||
if ($Context.Request.HttpMethod -ne 'POST') {
|
|
||||||
$Context.Response.StatusCode = 405
|
|
||||||
Send-Text $Context 'Use POST'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$rawBody = (New-Object IO.StreamReader $Context.Request.InputStream).ReadToEnd()
|
|
||||||
if (-not $rawBody) {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Missing request body.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$body = $rawBody | ConvertFrom-Json
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'Invalid JSON body.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$printers = $body.printers
|
|
||||||
if (-not $printers -or $printers.Count -eq 0) {
|
|
||||||
$Context.Response.StatusCode = 400
|
|
||||||
Send-Text $Context 'No printers specified.'
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Received request to install $($printers.Count) printers." Info Printers -LogToEvent
|
|
||||||
|
|
||||||
$successCount = 0
|
|
||||||
$failures = @()
|
|
||||||
|
|
||||||
foreach ($p in $printers) {
|
|
||||||
$clientCode = $p.ClientCode
|
|
||||||
$profileName = $p.ProfileName
|
|
||||||
$setDefault = $false
|
|
||||||
|
|
||||||
if ($p.PSObject.Properties.Name -contains 'SetAsDefault' -and $p.SetAsDefault) {
|
|
||||||
$setDefault = $true
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $clientCode -or -not $profileName) {
|
|
||||||
$msg = "Skipping printer entry because ClientCode or ProfileName is missing."
|
|
||||||
Write-LogHybrid $msg Warning Printers -LogToEvent
|
|
||||||
$failures += $msg
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
$summary = "ClientCode=$clientCode ProfileName=$profileName DisplayName=$($p.DisplayName) Location=$($p.Location) SetAsDefault=$setDefault"
|
|
||||||
Write-LogHybrid "Installing printer ($summary)" Info Printers -LogToEvent
|
|
||||||
|
|
||||||
try {
|
|
||||||
Invoke-SamyPrinterInstall `
|
|
||||||
-ClientCode $clientCode `
|
|
||||||
-ProfileName $profileName `
|
|
||||||
-SetAsDefault:$setDefault `
|
|
||||||
#-WhatIf
|
|
||||||
|
|
||||||
$successCount++
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
$errMsg = "Failed to install printer ($summary): $($_.Exception.Message)"
|
|
||||||
Write-LogHybrid $errMsg Error Printers -LogToEvent
|
|
||||||
$failures += $errMsg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = @{
|
|
||||||
SuccessCount = $successCount
|
|
||||||
FailureCount = $failures.Count
|
|
||||||
Failures = $failures
|
|
||||||
Message = "Printer install (WHATIF) processed. Check SAMY logs for detail."
|
|
||||||
}
|
|
||||||
|
|
||||||
Send-JSON $Context $result
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Invoke-InstallPrinters error: $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
$Context.Response.StatusCode = 500
|
|
||||||
Send-Text $Context "Internal server error installing printers."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SamyPrinterLocalConfigPath {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param()
|
|
||||||
|
|
||||||
$configDir = Join-Path $env:ProgramData 'SVS\Samy\Printers'
|
|
||||||
|
|
||||||
if (-not (Test-Path $configDir)) {
|
|
||||||
try {
|
|
||||||
New-Item -Path $configDir -ItemType Directory -Force | Out-Null
|
|
||||||
Write-LogHybrid "Created printer config folder at '$configDir'." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to create printer config folder '$configDir': $($_.Exception.Message)" Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (Join-Path $configDir 'printers.json')
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SamyPrinterConfigFromFile {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param()
|
|
||||||
|
|
||||||
$path = Get-SamyPrinterLocalConfigPath
|
|
||||||
|
|
||||||
if (-not (Test-Path $path)) {
|
|
||||||
throw "Local printer config file not found at '$path'. Create or update printers.json first."
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = Get-Content -Path $path -Raw -ErrorAction Stop
|
|
||||||
$profiles = $json | ConvertFrom-Json
|
|
||||||
|
|
||||||
if (-not $profiles) {
|
|
||||||
throw "Printer config file '$path' is empty or invalid JSON."
|
|
||||||
}
|
|
||||||
|
|
||||||
return $profiles
|
|
||||||
}
|
|
||||||
|
|
||||||
$Script:Samy_PrinterProfiles = $null
|
|
||||||
|
|
||||||
function Get-SamyPrinterProfiles {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[string]$ClientCode
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not $Script:Samy_PrinterProfiles) {
|
|
||||||
$Script:Samy_PrinterProfiles = Get-SamyPrinterConfigFromFile
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = $Script:Samy_PrinterProfiles
|
|
||||||
|
|
||||||
if ($PSBoundParameters.ContainsKey('ClientCode') -and $ClientCode) {
|
|
||||||
$result = $result | Where-Object { $_.ClientCode -eq $ClientCode }
|
|
||||||
}
|
|
||||||
|
|
||||||
return $result
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-SamyPrinterProfile {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][string]$ClientCode,
|
|
||||||
[Parameter(Mandatory)][string]$ProfileName
|
|
||||||
)
|
|
||||||
|
|
||||||
$profiles = Get-SamyPrinterProfiles -ClientCode $ClientCode
|
|
||||||
$match = $profiles | Where-Object { $_.ProfileName -eq $ProfileName }
|
|
||||||
|
|
||||||
if (-not $match) {
|
|
||||||
throw "No printer profile found for ClientCode '$ClientCode' and ProfileName '$ProfileName'."
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($match.Count -gt 1) {
|
|
||||||
throw "Multiple printer profiles found for ClientCode '$ClientCode' and ProfileName '$ProfileName'. De-duplicate in printers.json."
|
|
||||||
}
|
|
||||||
|
|
||||||
return $match
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ensure-SamyPrinterDriver {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[pscustomobject]$Profile
|
|
||||||
)
|
|
||||||
|
|
||||||
$driverName = $Profile.DriverName
|
|
||||||
if (-not $driverName) {
|
|
||||||
throw "Profile '$($Profile.ProfileName)' has no DriverName defined in printer config."
|
|
||||||
}
|
|
||||||
|
|
||||||
$existingDriver = Get-PrinterDriver -Name $driverName -ErrorAction SilentlyContinue
|
|
||||||
if ($existingDriver) {
|
|
||||||
Write-LogHybrid "Printer driver '$driverName' already installed." Info Printers -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Printer driver '$driverName' not found. Preparing to install." Info Printers -LogToEvent
|
|
||||||
|
|
||||||
$localDriverRoot = Get-SamyDriverFolderForProfile -Profile $Profile
|
|
||||||
|
|
||||||
$infPath = $null
|
|
||||||
if ($Profile.PSObject.Properties.Name -contains 'DriverInfPath' -and $Profile.DriverInfPath) {
|
|
||||||
if (Test-Path $Profile.DriverInfPath) {
|
|
||||||
$infPath = $Profile.DriverInfPath
|
|
||||||
Write-LogHybrid "Using existing INF path '$infPath' for driver '$driverName'." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "Configured DriverInfPath '$($Profile.DriverInfPath)' does not exist, will try repo download." Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$packageDownloaded = $false
|
|
||||||
|
|
||||||
if ($Profile.PSObject.Properties.Name -contains 'DriverPackagePath' -and $Profile.DriverPackagePath) {
|
|
||||||
$driverPackageUrl = "$Script:SamyRepoBase/$Script:SamyBranch/$($Profile.DriverPackagePath)?raw=1"
|
|
||||||
$localZip = Join-Path $localDriverRoot "package.zip"
|
|
||||||
|
|
||||||
Write-LogHybrid "Attempting to download driver package from $driverPackageUrl." Info Printers -LogToEvent
|
|
||||||
|
|
||||||
try {
|
|
||||||
Invoke-WebRequest -Uri $driverPackageUrl -OutFile $localZip -UseBasicParsing -ErrorAction Stop
|
|
||||||
Write-LogHybrid "Downloaded driver package from $driverPackageUrl to $localZip." Success Printers -LogToEvent
|
|
||||||
$packageDownloaded = $true
|
|
||||||
}
|
|
||||||
catch [System.Net.WebException] {
|
|
||||||
$response = $_.Exception.Response
|
|
||||||
$statusCode = $null
|
|
||||||
if ($response -and $response.StatusCode) {
|
|
||||||
$statusCode = [int]$response.StatusCode
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($statusCode -eq 404) {
|
|
||||||
Write-LogHybrid "Driver package not found at $driverPackageUrl (404). Falling back to INF-only install for '$($Profile.DisplayName)'." Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "Driver package download failed ($statusCode) from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Driver package download failed from $($driverPackageUrl): $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
throw "Failed to download driver package from $($driverPackageUrl): $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "No DriverPackagePath defined for '$($Profile.DisplayName)'; will rely on local INF." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($packageDownloaded) {
|
|
||||||
try {
|
|
||||||
Expand-Archive -Path $localZip -DestinationPath $localDriverRoot -Force
|
|
||||||
Write-LogHybrid "Expanded driver package to '$localDriverRoot'." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to expand driver package '$localZip': $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
throw "Failed to expand driver package '$localZip': $($_.Exception.Message)"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $infPath) {
|
|
||||||
if ($Profile.PSObject.Properties.Name -contains 'DriverInfName' -and $Profile.DriverInfName) {
|
|
||||||
|
|
||||||
$candidateInf = Join-Path $localDriverRoot $Profile.DriverInfName
|
|
||||||
if (Test-Path $candidateInf) {
|
|
||||||
$infPath = $candidateInf
|
|
||||||
Write-LogHybrid "Resolved INF from package as '$infPath' using DriverInfName '$($Profile.DriverInfName)' at root." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "Expected INF '$candidateInf' not found at root; searching recursively..." Warning Printers -LogToEvent
|
|
||||||
|
|
||||||
$found = Get-ChildItem -Path $localDriverRoot -Recurse -Filter $Profile.DriverInfName -File -ErrorAction SilentlyContinue |
|
|
||||||
Select-Object -First 1
|
|
||||||
|
|
||||||
if ($found) {
|
|
||||||
$infPath = $found.FullName
|
|
||||||
Write-LogHybrid "Resolved INF from package as '$infPath' (found by recursive search for '$($Profile.DriverInfName)')." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "Could not find any '$($Profile.DriverInfName)' under '$localDriverRoot' after expanding package." Error Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "DriverInfName not defined for profile '$($Profile.ProfileName)'; cannot auto-resolve INF from expanded package." Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $infPath -or -not (Test-Path $infPath)) {
|
|
||||||
throw "Driver '$driverName' is not installed and no valid DriverInfPath or usable driver package is available for profile '$($Profile.ProfileName)'."
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Installing printer driver '$driverName' from '$infPath'." Info Printers -LogToEvent
|
|
||||||
|
|
||||||
$pnputilCmd = "pnputil.exe /add-driver `"$infPath`" /install"
|
|
||||||
Write-LogHybrid "Running: $pnputilCmd" Info Printers -LogToEvent
|
|
||||||
|
|
||||||
$pnputilOutput = & pnputil.exe /add-driver "`"$infPath`"" /install 2>&1
|
|
||||||
$exitCode = $LASTEXITCODE
|
|
||||||
|
|
||||||
Write-LogHybrid "pnputil exit code: $exitCode. Output:`n$pnputilOutput" Info Printers -LogToEvent
|
|
||||||
|
|
||||||
if ($exitCode -ne 0) {
|
|
||||||
throw "pnputil failed with exit code $exitCode installing '$driverName' from '$infPath'."
|
|
||||||
}
|
|
||||||
|
|
||||||
$existingDriver = Get-PrinterDriver -ErrorAction SilentlyContinue | Where-Object {
|
|
||||||
$_.Name -eq $driverName -or
|
|
||||||
$_.Name -like "*$driverName*" -or
|
|
||||||
$driverName -like "*$($_.Name)*"
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $existingDriver) {
|
|
||||||
$sharpDrivers = Get-PrinterDriver -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object { $_.Name -like "SHARP*" }
|
|
||||||
|
|
||||||
$sharpList = if ($sharpDrivers) {
|
|
||||||
($sharpDrivers | Select-Object -ExpandProperty Name) -join ', '
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
'(none)'
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "After pnputil, driver '$driverName' not found. Existing SHARP drivers: $sharpList" Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "Printer driver '$($existingDriver.Name)' is present after pnputil (requested '$driverName')." Success Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Install-SamyTcpIpPrinter {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[pscustomobject]$Profile,
|
|
||||||
|
|
||||||
[switch]$SetAsDefault
|
|
||||||
)
|
|
||||||
|
|
||||||
$portName = $Profile.Address
|
|
||||||
$printerName = $Profile.DisplayName
|
|
||||||
|
|
||||||
if (-not $portName) {
|
|
||||||
throw "TCP/IP printer profile '$($Profile.ProfileName)' is missing Address in printer config."
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Get-PrinterPort -Name $portName -ErrorAction SilentlyContinue)) {
|
|
||||||
Write-Verbose "Creating TCP/IP port '$portName'."
|
|
||||||
Add-PrinterPort -Name $portName -PrinterHostAddress $Profile.Address
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "TCP/IP port '$portName' already exists."
|
|
||||||
}
|
|
||||||
|
|
||||||
$existingPrinter = Get-Printer -Name $printerName -ErrorAction SilentlyContinue
|
|
||||||
if ($existingPrinter) {
|
|
||||||
Write-Verbose "Printer '$printerName' already exists. Skipping creation."
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "Creating printer '$printerName' on port '$portName' using driver '$($Profile.DriverName)'."
|
|
||||||
Add-Printer -Name $printerName -PortName $portName -DriverName $Profile.DriverName
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($SetAsDefault -or $Profile.IsDefault) {
|
|
||||||
Write-Verbose "Setting '$printerName' as default printer."
|
|
||||||
(New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Install-SamySharedPrinter {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[pscustomobject]$Profile,
|
|
||||||
|
|
||||||
[switch]$SetAsDefault
|
|
||||||
)
|
|
||||||
|
|
||||||
if (-not $Profile.PrintServer -or -not $Profile.ShareName) {
|
|
||||||
throw "Shared printer profile '$($Profile.ProfileName)' is missing PrintServer or ShareName in printer config."
|
|
||||||
}
|
|
||||||
|
|
||||||
$connectionName = "\\$($Profile.PrintServer)\$($Profile.ShareName)"
|
|
||||||
|
|
||||||
$existing = Get-Printer -ErrorAction SilentlyContinue |
|
|
||||||
Where-Object {
|
|
||||||
$_.Name -eq $Profile.DisplayName -or
|
|
||||||
$_.ShareName -eq $Profile.ShareName
|
|
||||||
}
|
|
||||||
|
|
||||||
$printerName = $null
|
|
||||||
|
|
||||||
if ($existing) {
|
|
||||||
Write-Verbose "Shared printer '$($Profile.DisplayName)' already connected as '$($existing.Name)'."
|
|
||||||
$printerName = $existing.Name
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-Verbose "Adding shared printer connection '$connectionName'."
|
|
||||||
Add-Printer -ConnectionName $connectionName
|
|
||||||
|
|
||||||
$printerName = (Get-Printer |
|
|
||||||
Where-Object { $_.Name -like "*$($Profile.ShareName)*" } |
|
|
||||||
Select-Object -First 1
|
|
||||||
).Name
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($SetAsDefault -or $Profile.IsDefault) {
|
|
||||||
Write-Verbose "Setting '$printerName' as default printer."
|
|
||||||
(New-Object -ComObject WScript.Network).SetDefaultPrinter($printerName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Invoke-SamyPrinterInstall {
|
|
||||||
[CmdletBinding(SupportsShouldProcess = $true)]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ClientCode,
|
|
||||||
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[string]$ProfileName,
|
|
||||||
|
|
||||||
[switch]$SetAsDefault
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
$profile = Get-SamyPrinterProfile -ClientCode $ClientCode -ProfileName $ProfileName
|
|
||||||
$targetName = $profile.DisplayName
|
|
||||||
|
|
||||||
if ($PSCmdlet.ShouldProcess($targetName, "Install printer")) {
|
|
||||||
|
|
||||||
Write-LogHybrid "Installing printer profile '$($profile.ProfileName)' for client '$ClientCode' (WhatIf=$WhatIfPreference)." Info Printers -LogToEvent
|
|
||||||
|
|
||||||
Ensure-SamyPrinterDriver -Profile $profile
|
|
||||||
|
|
||||||
switch ($profile.Type) {
|
|
||||||
'TcpIp' {
|
|
||||||
Install-SamyTcpIpPrinter -Profile $profile -SetAsDefault:$SetAsDefault
|
|
||||||
}
|
|
||||||
'Shared' {
|
|
||||||
Install-SamySharedPrinter -Profile $profile -SetAsDefault:$SetAsDefault
|
|
||||||
}
|
|
||||||
default {
|
|
||||||
throw "Unsupported printer Type '$($profile.Type)' in profile '$($profile.ProfileName)'. Use 'TcpIp' or 'Shared'."
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Installed printer '$($profile.DisplayName)' (Client=$ClientCode Profile=$ProfileName)." Info Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid (
|
|
||||||
"Printer install failed for Client={0} Profile={1}: {2}" -f $ClientCode, $ProfileName, $_.Exception.Message
|
|
||||||
) Error Printers -LogToEvent
|
|
||||||
throw
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Update-SamyPrinterConfig {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)]
|
|
||||||
[object]$PrinterProfiles,
|
|
||||||
|
|
||||||
[switch]$SkipIfEmpty
|
|
||||||
)
|
|
||||||
|
|
||||||
$path = Get-SamyPrinterLocalConfigPath
|
|
||||||
|
|
||||||
$profilesArray = @($PrinterProfiles)
|
|
||||||
|
|
||||||
if ($SkipIfEmpty -and ($null -eq $PrinterProfiles -or $profilesArray.Count -eq 0)) {
|
|
||||||
Write-LogHybrid "Update-SamyPrinterConfig: no printer profiles returned; keeping existing printers.json." Warning Printers -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($profilesArray.Count -eq 0) {
|
|
||||||
Write-LogHybrid "Update-SamyPrinterConfig: zero profiles; writing empty printers.json." Warning Printers -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
$profilesArray | ConvertTo-Json -Depth 5 | Set-Content -Path $path -Encoding UTF8
|
|
||||||
Write-LogHybrid "Saved $($profilesArray.Count) printer profiles to '$path'." Success Printers -LogToEvent
|
|
||||||
|
|
||||||
$Script:Samy_PrinterProfiles = $null
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to write printers.json to '$path': $($_.Exception.Message)" Error Printers -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
# Samy.SVSBootstrap.ps1
|
|
||||||
# SVSMSP toolkit bootstrap and cleanup
|
|
||||||
|
|
||||||
function Initialize-NuGetProvider {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param()
|
|
||||||
|
|
||||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
||||||
$ProgressPreference = 'SilentlyContinue'
|
|
||||||
$ConfirmPreference = 'None'
|
|
||||||
|
|
||||||
$provPath = "$env:ProgramData\PackageManagement\ProviderAssemblies"
|
|
||||||
if (-not (Test-Path $provPath)) {
|
|
||||||
try {
|
|
||||||
New-Item -Path $provPath -ItemType Directory -Force -ErrorAction Stop | Out-Null
|
|
||||||
Write-LogHybrid "Created missing provider folder: $provPath" Info Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to create provider folder: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not (Get-Command Install-PackageProvider -ErrorAction SilentlyContinue)) {
|
|
||||||
try {
|
|
||||||
Install-Module PowerShellGet -Force -AllowClobber -Confirm:$false -ErrorAction Stop
|
|
||||||
Write-LogHybrid "Installed PowerShellGet module" Info Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "PowerShellGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$pkgMgmtVersion = (Get-Module PackageManagement -ListAvailable | Sort-Object Version -Descending | Select-Object -First 1).Version
|
|
||||||
if ($pkgMgmtVersion -lt [Version]"1.3.1") {
|
|
||||||
try {
|
|
||||||
Install-Module PackageManagement -Force -AllowClobber -Confirm:$false -ErrorAction Stop
|
|
||||||
Write-LogHybrid "Updated PackageManagement to latest version" Info Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "PackageManagement update failed: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Import-Module PackageManagement -Force -ErrorAction SilentlyContinue | Out-Null
|
|
||||||
Import-Module PowerShellGet -Force -ErrorAction SilentlyContinue | Out-Null
|
|
||||||
|
|
||||||
$gallery = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
|
|
||||||
if ($gallery -and $gallery.InstallationPolicy -ne 'Trusted') {
|
|
||||||
try {
|
|
||||||
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction Stop
|
|
||||||
Write-LogHybrid "PSGallery marked as Trusted" Info Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to trust PSGallery: $($_.Exception.Message)" Warning Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
|
|
||||||
if (-not $nuget) {
|
|
||||||
try {
|
|
||||||
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Confirm:$false -ErrorAction Stop
|
|
||||||
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue
|
|
||||||
Write-LogHybrid "Installed NuGet provider v$($nuget.Version)" Info Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "NuGet install failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "NuGet provider already present (v$($nuget.Version))" Info Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
Import-PackageProvider -Name NuGet -Force -ErrorAction Stop | Out-Null
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "NuGet provider import failed: $($_.Exception.Message)" Error Bootstrap -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 Start-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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$cscePath = 'C:\CSCE'
|
|
||||||
if (Test-Path $cscePath) {
|
|
||||||
try {
|
|
||||||
Remove-Item -Path $cscePath -Recurse -Force
|
|
||||||
Write-LogHybrid "Deleted '$cscePath' contents." Success SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to delete '$cscePath': $($_.Exception.Message)" Warning SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Remove-SVSDeploymentRegKey {
|
|
||||||
$regKey = 'HKLM:\Software\SVS'
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (Test-Path $regKey) {
|
|
||||||
Remove-Item -Path $regKey -Recurse -Force
|
|
||||||
Write-LogHybrid "Registry key '$regKey' deleted successfully." Success SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
Write-LogHybrid "Registry key '$regKey' not found; nothing to delete." Info SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to delete registry key '$regKey': $($_.Exception.Message)" Error SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Repair-SVSMspEventLogBinding {
|
|
||||||
param(
|
|
||||||
[string]$EventSource = "SVSMSP_Module",
|
|
||||||
[string]$TargetLog = "SVSMSP Events"
|
|
||||||
)
|
|
||||||
|
|
||||||
Write-LogHybrid "Checking Event Log binding for source '$EventSource'..." Info SVSModule -LogToEvent
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) {
|
|
||||||
Write-LogHybrid "Event source '$EventSource' not found. Nothing to repair." Info SVSModule -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
$currentLog = [System.Diagnostics.EventLog]::LogNameFromSourceName($EventSource, '.')
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to query Event Log binding for '$EventSource': $($_.Exception.Message)" Warning SVSModule -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (-not $currentLog) {
|
|
||||||
Write-LogHybrid "Could not determine current log for event source '$EventSource'. Skipping repair." Warning SVSModule -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($currentLog -eq $TargetLog) {
|
|
||||||
Write-LogHybrid "Event source '$EventSource' already bound to '$TargetLog'." Info SVSModule -LogToEvent
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Rebinding event source '$EventSource' from '$currentLog' to '$TargetLog'..." Warning SVSModule -LogToEvent
|
|
||||||
|
|
||||||
try {
|
|
||||||
[System.Diagnostics.EventLog]::DeleteEventSource($EventSource)
|
|
||||||
|
|
||||||
if (-not [System.Diagnostics.EventLog]::Exists($TargetLog)) {
|
|
||||||
New-EventLog -LogName $TargetLog -Source $EventSource -ErrorAction Stop
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
New-EventLog -LogName $TargetLog -Source $EventSource -ErrorAction Stop
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Event source '$EventSource' rebound to '$TargetLog'." Success SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Failed to rebind event source '$EventSource' to log '$TargetLog': $($_.Exception.Message)" Error SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Start-ToolkitInstallation {
|
|
||||||
Initialize-NuGetProvider
|
|
||||||
Start-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
|
|
||||||
|
|
||||||
Repair-SVSMspEventLogBinding -EventSource "SVSMSP_Module" -TargetLog "SVSMSP Events"
|
|
||||||
|
|
||||||
Write-LogHybrid "Toolkit installation completed." Success SVSModule -LogToEvent
|
|
||||||
}
|
|
||||||
|
|
||||||
Write-LogHybrid "Install-SVSMSP called" Info SVSModule -LogToEvent
|
|
||||||
|
|
||||||
if ($Cleanup) {
|
|
||||||
Start-Cleanup
|
|
||||||
Remove-SVSDeploymentRegKey
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($InstallToolkit) {
|
|
||||||
Start-ToolkitInstallation
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
Start-ToolkitInstallation
|
|
||||||
}
|
|
||||||
@@ -1,348 +0,0 @@
|
|||||||
# Samy.UI.ps1
|
|
||||||
# Task metadata and UI HTML generation
|
|
||||||
|
|
||||||
# Global task definitions used by UI and headless offboard
|
|
||||||
$Global:SamyTasks = @(
|
|
||||||
# On-Boarding, left column
|
|
||||||
@{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Invoke-setSVSPowerPlan'; Page='onboard'; Column='left' },
|
|
||||||
@{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Invoke-InstallSVSMSP'; Page='onboard'; Column='left' },
|
|
||||||
@{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Invoke-InstallCyberQP'; Page='onboard'; Column='left' },
|
|
||||||
@{ Id='installHelpDesk'; Name='installHelpDesk'; Label='Install HelpDesk'; HandlerFn='Invoke-InstallHelpDesk'; Page='onboard'; Column='left' },
|
|
||||||
@{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Invoke-InstallThreatLocker'; Page='onboard'; Column='left' },
|
|
||||||
@{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Invoke-InstallRocketCyber'; Page='onboard'; Column='left' },
|
|
||||||
@{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Invoke-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
|
|
||||||
@{ 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='Invoke-SetEdgeDefaultSearchEngine'; Page='onboard'; Column='right' },
|
|
||||||
|
|
||||||
# Off-Boarding
|
|
||||||
@{ Id='offUninstallCyberQP'; Name='offUninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Invoke-UninstallCyberQP'; Page='offboard' },
|
|
||||||
@{ Id='offUninstallHelpDesk'; Name='offUninstallHelpDesk'; Label='Uninstall HelpDesk'; HandlerFn='Invoke-UninstallHelpDesk'; Page='offboard' },
|
|
||||||
@{ Id='offUninstallThreatLocker'; Name='offUninstallThreatLocker'; Label='Uninstall ThreatLocker'; HandlerFn='Invoke-UninstallThreatLocker'; Page='offboard' },
|
|
||||||
@{ Id='offUninstallRocketCyber'; Name='offUninstallRocketCyber'; Label='Uninstall RocketCyber'; HandlerFn='Invoke-UninstallRocketCyber'; Page='offboard' },
|
|
||||||
@{ Id='offCleanupSVSMSPModule'; Name='offCleanupSVSMSPModule'; Label='Cleanup SVSMSP Toolkit'; HandlerFn='Invoke-CleanupSVSMSP'; 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='Invoke-InstallChrome'; Page='SVSApps' },
|
|
||||||
@{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Invoke-InstallAcrobat'; Page='SVSApps' }
|
|
||||||
)
|
|
||||||
|
|
||||||
Write-LogHybrid "Tasks by page: onboard=$(
|
|
||||||
($Global:SamyTasks | Where-Object Page -eq 'onboard').Count
|
|
||||||
) offboard=$(
|
|
||||||
($Global:SamyTasks | Where-Object Page -eq 'offboard').Count
|
|
||||||
) tweaks=$(
|
|
||||||
($Global:SamyTasks | Where-Object Page -eq 'tweaks').Count
|
|
||||||
) apps=$(
|
|
||||||
($Global:SamyTasks | Where-Object Page -eq 'SVSApps').Count
|
|
||||||
)" Info UI -LogToEvent
|
|
||||||
|
|
||||||
function Publish-Checkboxes {
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory)][string]$Page,
|
|
||||||
[string]$Column
|
|
||||||
)
|
|
||||||
|
|
||||||
$tasks = $Global:SamyTasks | Where-Object Page -EQ $Page
|
|
||||||
|
|
||||||
if (-not [string]::IsNullOrEmpty($Column)) {
|
|
||||||
$tasks = $tasks | Where-Object Column -EQ $Column
|
|
||||||
}
|
|
||||||
|
|
||||||
(
|
|
||||||
$tasks |
|
|
||||||
ForEach-Object {
|
|
||||||
$taskId = $_.Id
|
|
||||||
$tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) {
|
|
||||||
" title='$($_.Tooltip)'"
|
|
||||||
} else { '' }
|
|
||||||
|
|
||||||
$html = "<label$tooltip><input type='checkbox' id='$taskId' name='$($_.Name)' data-column='$Column'> $($_.Label)</label>"
|
|
||||||
|
|
||||||
if ($_.SubOptions) {
|
|
||||||
$subHtml = (
|
|
||||||
$_.SubOptions |
|
|
||||||
ForEach-Object {
|
|
||||||
"<label style='margin-left:20px; display:block;'>
|
|
||||||
<input type='checkbox' class='sub-option-$taskId' name='$($_.Value)' value='$($_.Value)'> $($_.Label)
|
|
||||||
</label>"
|
|
||||||
}
|
|
||||||
) -join "`n"
|
|
||||||
|
|
||||||
$html += @"
|
|
||||||
<div id='${taskId}OptionsContainer' style='display:none; margin-top:4px;'>
|
|
||||||
$subHtml
|
|
||||||
</div>
|
|
||||||
"@
|
|
||||||
}
|
|
||||||
|
|
||||||
$html
|
|
||||||
}
|
|
||||||
) -join "`n"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-ModuleVersionHtml {
|
|
||||||
$mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1
|
|
||||||
|
|
||||||
$branchDisplay = switch ($Script:SamyBranch.ToLower()) {
|
|
||||||
'main' { 'Main / Stable' }
|
|
||||||
'beta' { 'Beta' }
|
|
||||||
default { $Script:SamyBranch }
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($mod) {
|
|
||||||
return "<div style='color:#bbb; font-size:0.9em; margin-top:1em;'>
|
|
||||||
Module Version: $($mod.Version)<br>
|
|
||||||
UI Branch: $branchDisplay
|
|
||||||
</div>"
|
|
||||||
}
|
|
||||||
|
|
||||||
return "<div style='color:#f66;'>SVSMSP_Module not found</div>"
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-RemoteText {
|
|
||||||
[CmdletBinding()]
|
|
||||||
param(
|
|
||||||
[Parameter(Mandatory = $true)][string]$Url
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
|
||||||
$resp = Invoke-WebRequest -Uri $Url -UseBasicParsing -ErrorAction Stop
|
|
||||||
return $resp.Content
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
Write-LogHybrid "Get-RemoteText failed for ${Url}: $($_.Exception.Message)" Warning UI -LogToEvent
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function Get-UIHtml {
|
|
||||||
param([string]$Page = 'onboard')
|
|
||||||
if (-not $Page) { $Page = 'onboard' }
|
|
||||||
|
|
||||||
$onboardLeft = Publish-Checkboxes -Page 'onboard' -Column 'left'
|
|
||||||
$onboardRight = Publish-Checkboxes -Page 'onboard' -Column 'right'
|
|
||||||
$offboard = Publish-Checkboxes -Page 'offboard' -Column ''
|
|
||||||
$tweaks = Publish-Checkboxes -Page 'tweaks' -Column ''
|
|
||||||
$apps = Publish-Checkboxes -Page 'SVSApps' -Column ''
|
|
||||||
|
|
||||||
$tasksJsAll = (
|
|
||||||
$Global:SamyTasks | ForEach-Object {
|
|
||||||
" { id: '$($_.Id)', handler: '/$($_.Name)', label: '$($_.Label)' }"
|
|
||||||
}
|
|
||||||
) -join ",`n"
|
|
||||||
|
|
||||||
$cssContent = Get-RemoteText -Url $Script:SamyCssUrl
|
|
||||||
$jsContent = Get-RemoteText -Url $Script:SamyJsUrl
|
|
||||||
|
|
||||||
if ($cssContent) {
|
|
||||||
$pattern = 'background-image:\s*url\("SAMY\.png"\);?'
|
|
||||||
$replacement = "background-image: url('$Script:SamyBgLogoUrl');"
|
|
||||||
$cssContent = [regex]::Replace($cssContent, $pattern, $replacement)
|
|
||||||
}
|
|
||||||
|
|
||||||
$htmlTemplate = @"
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
||||||
<title>Script Automation Monkey</title>
|
|
||||||
<link rel="icon" href="$Script:SamyFaviconUrl">
|
|
||||||
<style>
|
|
||||||
$cssContent
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="logo-container">
|
|
||||||
<div class="logo-left">
|
|
||||||
<img src="$Script:SamyTopLogoUrl" alt="SVS Logo">
|
|
||||||
{{moduleVersion}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tagline" class="tagline">
|
|
||||||
Script Automation Monkey (Yeah!)
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="samyHint" class="samy-hint"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="container">
|
|
||||||
<div class="sidebar">
|
|
||||||
<button class="tab-button" data-tab="onboardTab">On-Boarding</button>
|
|
||||||
<button class="tab-button" data-tab="offboardTab">Off-Boarding</button>
|
|
||||||
<button class="tab-button" data-tab="tweaksTab">Tweaks</button>
|
|
||||||
<button class="tab-button" data-tab="SVSAppsTab">SVS APPs</button>
|
|
||||||
<button class="tab-button" data-tab="devicesTab">Devices</button>
|
|
||||||
|
|
||||||
<div id="status-box" style="margin-top: 1em; font-family: monospace;"></div>
|
|
||||||
</div>
|
|
||||||
<div class="content">
|
|
||||||
<div id="onboardTab" class="tab-content">
|
|
||||||
<h2>On-Boarding</h2>
|
|
||||||
<h3 class="subtitle">This new deployment method ensures everything is successfully deployed with greater ease!</h3>
|
|
||||||
|
|
||||||
<div class="columns-container">
|
|
||||||
<div class="checkbox-group column">
|
|
||||||
<h3>SVSMSP Stack</h3>
|
|
||||||
<label><input type="checkbox" id="selectAllLeftCheckbox" onclick="toggleColumn('left')"> Select All</label>
|
|
||||||
{{onboardLeftColumn}}
|
|
||||||
</div>
|
|
||||||
<div class="checkbox-group column">
|
|
||||||
<h3>Optional</h3>
|
|
||||||
<label><input type="checkbox" id="selectAllRightCheckbox" onclick="toggleColumn('right')"> Select All</label>
|
|
||||||
{{onboardRightColumn}}
|
|
||||||
|
|
||||||
<div class="option">
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="chkRenameComputer" data-column="right">
|
|
||||||
Rename Computer
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="renameComputerBlock" style="display:none; margin-left: 24px; margin-top: 6px;">
|
|
||||||
<label for="txtNewComputerName">New computer name:</label>
|
|
||||||
<input type="text" id="txtNewComputerName" placeholder="e.g. CORP-LAP-123" />
|
|
||||||
<small style="display:block; margin-top:4px;">
|
|
||||||
(Max 15 chars; letters, numbers, and hyphens only.)
|
|
||||||
</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="PasswordContainer" style="display:none; margin-bottom:1em;">
|
|
||||||
<label for="Password">Enter Password:</label>
|
|
||||||
<div style="display:flex; gap:5px;">
|
|
||||||
<input type="password" id="Password" placeholder="Enter Password" style="flex:1;" />
|
|
||||||
<button onclick="fetchSites()" class="go-button">GO!</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="dattoRmmContainer" style="display:none; margin-bottom:1em;">
|
|
||||||
<label for="dattoDropdown">Select a Datto RMM site:</label>
|
|
||||||
<select id="dattoDropdown" style="width:100%;">
|
|
||||||
<option disabled selected>Fetching sites...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="offboardTab" class="tab-content">
|
|
||||||
<h2>Off-Boarding</h2>
|
|
||||||
<div class="columns-container">
|
|
||||||
<div class="checkbox-group column">
|
|
||||||
<h3>Remove Stack</h3>
|
|
||||||
<label>
|
|
||||||
<input type="checkbox" id="offboardSelectAll" onclick="toggleOffboardAll()">
|
|
||||||
Select All
|
|
||||||
</label>
|
|
||||||
{{offboardCheckboxes}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="tweaksTab" class="tab-content">
|
|
||||||
<h2>Tweaks</h2>
|
|
||||||
<div class="columns-container">
|
|
||||||
<div class="checkbox-group column">
|
|
||||||
<h3>Tweaks</h3>
|
|
||||||
{{tweaksCheckboxes}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="SVSAppsTab" class="tab-content">
|
|
||||||
<h2>SVS APPs</h2>
|
|
||||||
<div class="columns-container">
|
|
||||||
<div class="checkbox-group column">
|
|
||||||
<h3>Applications</h3>
|
|
||||||
{{appsCheckboxes}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="devicesTab" class="tab-content">
|
|
||||||
<h2>Devices</h2>
|
|
||||||
<h3 class="subtitle">Manage printers and other client devices.</h3>
|
|
||||||
|
|
||||||
<div id="printerPasswordContainer" style="margin-bottom:1em;">
|
|
||||||
<label for="PrinterPassword">Enter Printer Password:</label>
|
|
||||||
<div style="display:flex; gap:5px;">
|
|
||||||
<input type="password"
|
|
||||||
id="PrinterPassword"
|
|
||||||
placeholder="Enter printer password"
|
|
||||||
style="flex:1;" />
|
|
||||||
<button onclick="fetchPrinters()" class="go-button">Get Printers</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="printerClientContainer" style="display:none; margin-bottom:1em;">
|
|
||||||
<label for="printerClientDropdown">Select Client:</label>
|
|
||||||
<select id="printerClientDropdown" style="width:100%;">
|
|
||||||
<option disabled selected>Fetch printers first...</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="printerListContainer" style="display:none; margin-bottom:1em;">
|
|
||||||
<label>Printers for selected client:</label>
|
|
||||||
<small style="display:block; margin-bottom:4px; opacity:0.8;">
|
|
||||||
Check the printers to install, and mark one as "Make default" (optional).
|
|
||||||
</small>
|
|
||||||
|
|
||||||
<div id="printerCheckboxContainer"
|
|
||||||
style="max-height:200px; overflow-y:auto; border:1px solid #444; padding:6px; border-radius:4px;">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button id="installPrintersButton"
|
|
||||||
class="go-button"
|
|
||||||
style="margin-top:8px;"
|
|
||||||
onclick="installSelectedPrinters()">
|
|
||||||
Install Selected Printers
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
window.SAMY_TASKS = [
|
|
||||||
{{tasksJsAll}}
|
|
||||||
];
|
|
||||||
window.SAMY_DEFAULT_PAGE = "{{defaultPage}}";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
$jsContent
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="fixed-buttons">
|
|
||||||
<button class="exit-button" onclick="endSession()">Exit</button>
|
|
||||||
<button class="run-button" onclick="triggerInstall()">Run Selected</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
"@
|
|
||||||
|
|
||||||
$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
|
|
||||||
}
|
|
||||||
396
samy-mini.ps1
396
samy-mini.ps1
@@ -1,396 +0,0 @@
|
|||||||
<#
|
|
||||||
.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
|
|
||||||
17
samy.ps1
17
samy.ps1
@@ -239,7 +239,7 @@ $ConfirmPreference = 'None'
|
|||||||
$Global:DattoWebhookUrl = 'https://bananas.svstools.ca/dattormm'
|
$Global:DattoWebhookUrl = 'https://bananas.svstools.ca/dattormm'
|
||||||
|
|
||||||
# SAMY asset config (change branch or base once and it updates everything)
|
# SAMY asset config (change branch or base once and it updates everything)
|
||||||
$Script:SamyBranch = 'beta' # 'main' or 'beta'
|
$Script:SamyBranch = 'main' # 'beta'
|
||||||
$Script:SamyRepoBase = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch'
|
$Script:SamyRepoBase = 'https://git.svstools.ca/SVS_Public_Repo/SAMY/raw/branch'
|
||||||
|
|
||||||
|
|
||||||
@@ -247,7 +247,7 @@ $ConfirmPreference = 'None'
|
|||||||
$Script:SamyTopLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_logo.svg"
|
$Script:SamyTopLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_logo.svg"
|
||||||
|
|
||||||
# Background SAMY image used in CSS
|
# Background SAMY image used in CSS
|
||||||
$Script:SamyBgLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SAMY.png"
|
$Script:SamyBgLogoUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SAMY5.png?raw=1"
|
||||||
|
|
||||||
$Script:SamyFaviconUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_Favicon.ico"
|
$Script:SamyFaviconUrl = "$Script:SamyRepoBase/$Script:SamyBranch/SVS_Favicon.ico"
|
||||||
$Script:SamyCssUrl = "$Script:SamyRepoBase/$Script:SamyBranch/samy.css?raw=1"
|
$Script:SamyCssUrl = "$Script:SamyRepoBase/$Script:SamyBranch/samy.css?raw=1"
|
||||||
@@ -1068,12 +1068,15 @@ function Get-UIHtml {
|
|||||||
$cssContent = Get-RemoteText -Url $Script:SamyCssUrl
|
$cssContent = Get-RemoteText -Url $Script:SamyCssUrl
|
||||||
$jsContent = Get-RemoteText -Url $Script:SamyJsUrl
|
$jsContent = Get-RemoteText -Url $Script:SamyJsUrl
|
||||||
|
|
||||||
# Make the CSS background-image follow $Script:SamyBgLogoUrl
|
|
||||||
if ($cssContent) {
|
if ($cssContent) {
|
||||||
$pattern = 'background-image:\s*url\("SAMY\.png"\);?' # matches with or without extra spaces/semicolon
|
$cssContent += @"
|
||||||
$replacement = "background-image: url('$Script:SamyBgLogoUrl');"
|
|
||||||
$cssContent = [regex]::Replace($cssContent, $pattern, $replacement)
|
/* SAMY background override injected by script */
|
||||||
}
|
.sidebar::after {
|
||||||
|
background-image: url('$Script:SamyBgLogoUrl') !important;
|
||||||
|
}
|
||||||
|
"@
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#
|
#
|
||||||
|
|||||||
Reference in New Issue
Block a user