diff --git a/Stackmonkey.ps1 b/Stackmonkey.ps1 index 792639f..79fab72 100644 --- a/Stackmonkey.ps1 +++ b/Stackmonkey.ps1 @@ -1,11 +1,8 @@ -#region ScriptMonkey.ps1 (full updated script) - -#region changes to be done (now implemented) -# - Event logging uses custom Windows log "SVS Scripting". -# - Added SVS APPs tasks: -# winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements -# winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements -#endregion +#region changes to be done +# We could change line 298 and 379 the have it log in "SVS Scripting" instead of "Application" if we can find a way to force create the log +# winget install --id=Google.Chrome --silent --accept-package-agreements --accept-source-agreements +# winget install --id=Adobe.Acrobat.Reader.64-bit --silent --accept-package-agreements --accept-source-agreements +#endregion changes to be done <# .SYNOPSIS @@ -32,410 +29,388 @@ clear success/failure messages via Write-LogHybrid. #> -#region Safely bypass Restricted Execution Policy + Elevation -# ─── Relaunch elevated (Admin) + with ExecutionPolicy Bypass when needed ─── -$needBypass = ($ExecutionContext.SessionState.LanguageMode -ne 'FullLanguage' -or (Get-ExecutionPolicy) -eq 'Restricted') -$identity = [Security.Principal.WindowsIdentity]::GetCurrent() -$principal = [Security.Principal.WindowsPrincipal]$identity -$needAdmin = -not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) +#region Safely bypass Restricted Execution Policy +# ─── 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://sm.svstools.com' -UseBasicParsing | iex }" + } -if ($needBypass -or $needAdmin) { - Write-Host "[Info] Relaunching elevated (Admin) with ExecutionPolicy Bypass..." -ForegroundColor Yellow - $args = if ($PSCommandPath) { "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`"" } else { "-NoProfile -ExecutionPolicy Bypass -Command `"& { iwr 'https://sm.svstools.com' -UseBasicParsing | iex }`"" } - Start-Process -FilePath "powershell.exe" -ArgumentList $args -Verb RunAs | Out-Null exit } -# ─── TLS and silent defaults ─── +# ─── TLS and silent install defaults ─── [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 $ProgressPreference = 'SilentlyContinue' $ConfirmPreference = 'None' -#endregion Safely bypass Restricted Execution Policy + Elevation +#endregion Safely bypass Restricted Execution Policy function Invoke-ScriptMonkey { - # ───────────────────────────────────────────────────────────────────────── # PARAMETERS + GLOBAL VARIABLES # ───────────────────────────────────────────────────────────────────────── - [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, - - # ───────────────────────────────────────────────────────── - # Datto headless mode - - # ─── DattoFetch & DattoInstall share the webhook creds ───────────── - [Parameter(Mandatory,ParameterSetName='DattoFetch')] - [Parameter(Mandatory,ParameterSetName='DattoInstall')] - [switch]$UseWebhook, - - [Parameter(Mandatory,ParameterSetName='DattoFetch')] - [Parameter(Mandatory,ParameterSetName='DattoInstall')] - [string]$WebhookPassword, - - [string]$WebhookUrl = $Global:DattoWebhookUrl, - - # ─── only DattoFetch uses these ──────────────────────────────────── - [Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites, - [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList, - [Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv', - - # ─── only DattoInstall uses these ───────────────────────────────── - [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 - ) - - #region global variables - - # Listening port for HTTP UI - $Port = 8082 - - # Configurable endpoints - $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' - - # Initialize a global in-memory log cache - if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { - $Global:LogCache = [System.Collections.ArrayList]::new() - } - #endregion global variables - - #region SVS Module - - 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 Perform-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 - } - } - } - - function Perform-ToolkitInstallation { - Perform-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 - Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" -LogToEvent - } - - Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent - if ($Cleanup) { Perform-Cleanup; return } - if ($InstallToolkit) { Perform-ToolkitInstallation; return } - Perform-ToolkitInstallation - } - - #endregion SVS Module - - #region Write-Log - - function Write-LogHelper { - [CmdletBinding()] + [CmdletBinding( + DefaultParameterSetName='UI', + SupportsShouldProcess=$true, + ConfirmImpact= 'Medium' + )] param( - [Parameter(Mandatory)][string]$Message, - [ValidateSet("Info","Warning","Error","Success","General")] - [string]$Level = "Info", - [string]$TaskCategory = "GeneralTask", - [switch]$LogToEvent, - [string]$EventSource = "Script Automation Monkey", - [string]$EventLog = "SVS Scripting", - [int] $CustomEventID, - [string]$LogFile, - [switch]$PassThru + # ───────────────────────────────────────────────────────── + # Toolkit-only mode + [Parameter(Mandatory,ParameterSetName='Toolkit')][switch]$SilentInstall, + + # ───────────────────────────────────────────────────────── + # remove Toolkit + [Parameter(Mandatory,ParameterSetName='Cleanup')][switch]$Cleanup, + + # ───────────────────────────────────────────────────────── + # Datto headless mode + + # ─── DattoFetch & DattoInstall share the webhook creds ───────────── + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [switch]$UseWebhook, + + [Parameter(Mandatory,ParameterSetName='DattoFetch')] + [Parameter(Mandatory,ParameterSetName='DattoInstall')] + [string]$WebhookPassword, + + [string]$WebhookUrl = $Global:DattoWebhookUrl, + + # ─── only DattoFetch uses these ──────────────────────────────────── + [Parameter(ParameterSetName='DattoFetch')][switch]$FetchSites, + [Parameter(ParameterSetName='DattoFetch')][switch] $SaveSitesList, + [Parameter(ParameterSetName='DattoFetch')][ValidatePattern('\.csv$|\.json$')][string] $OutputFile = 'datto_sites.csv', + + # ─── only DattoInstall uses these ───────────────────────────────── + [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 ) - - $idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 } - $colMap = @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" } - $EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] } - $color = $colMap[$Level] - $fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" - - Write-Host $fmt -ForegroundColor $color - + + #region global variables + $Port = 8082 + $Global:DattoWebhookUrl = 'https://automate.svstools.ca/webhook/svsmspkit' if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { $Global:LogCache = [System.Collections.ArrayList]::new() } - $Global:LogCache.Add([pscustomobject]@{ - Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') - Level = $Level - Message = $fmt - }) | Out-Null + #endregion global variables - if ($PSBoundParameters.LogFile) { - try { - "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" | - Out-File -FilePath $LogFile -Append -Encoding UTF8 - } - catch { Write-Host "[Warning] File log failed: $_" -ForegroundColor Yellow } - } + #region SVS Module + 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/" + ) - if ($LogToEvent) { - try { - if (-not [System.Diagnostics.EventLog]::SourceExists($EventSource)) { - New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop + function Perform-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 + } } - } catch { - Write-Host "[Warning] Could not create event log '$EventLog' or source '$EventSource': $($_.Exception.Message)" -ForegroundColor Yellow - return } - $entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' } - - try { - Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $entryType -EventID $EventID -Message $fmt - } catch { - Write-Host "[Warning] EventLog failed: $($_.Exception.Message)" -ForegroundColor Yellow + function Perform-ToolkitInstallation { + Perform-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 + Write-LogHybrid "Toolkit installation complete." "Success" "SVSModule" -LogToEvent } + + Write-LogHybrid "Install-SVSMSP called" "Info" "SVSModule" -LogToEvent + if ($Cleanup) { Perform-Cleanup; return } + if ($InstallToolkit) { Perform-ToolkitInstallation; return } + Perform-ToolkitInstallation + } + #endregion SVS Module + + #region Write-Log + function Write-LogHelper { + [CmdletBinding()] + param( + [Parameter(Mandatory)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "Script Automation Monkey", + [string]$EventLog = "SVS Scripting", # <— default changed from Application + [int] $CustomEventID, + [string]$LogFile, + [switch]$PassThru + ) + + $idMap = @{ Info=1000; Warning=2000; Error=3000; Success=4000; General=1000 } + $colMap = @{ Info="Cyan"; Warning="Yellow"; Error="Red"; Success="Green"; General="White" } + $EventID = if ($PSBoundParameters.CustomEventID) { $CustomEventID } else { $idMap[$Level] } + $color = $colMap[$Level] + $fmt = "[$Level] [$TaskCategory] $Message (Event ID: $EventID)" + + Write-Host $fmt -ForegroundColor $color + + if (-not $Global:LogCache -or -not ($Global:LogCache -is [System.Collections.ArrayList])) { + $Global:LogCache = [System.Collections.ArrayList]::new() + } + $Global:LogCache.Add([pscustomobject]@{ + Timestamp = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') + Level = $Level + Message = $fmt + }) | Out-Null + + if ($PSBoundParameters.LogFile) { + try { + "$((Get-Date).ToString('yyyy-MM-dd HH:mm:ss')) $fmt" | Out-File -FilePath $LogFile -Append -Encoding UTF8 + } catch { + Write-Host "[Warning] File log failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + if ($LogToEvent) { + try { + # Ensure the log & source exist and are mapped together. + $regBase = 'HKLM:\SYSTEM\CurrentControlSet\Services\EventLog' + $mappedLog = $null + if ([System.Diagnostics.EventLog]::SourceExists($EventSource)) { + $mappedLog = (Get-ChildItem $regBase -ErrorAction SilentlyContinue | + Where-Object { Test-Path (Join-Path $_.PSPath $EventSource) } | + Select-Object -First 1 + ).PSChildName + } + if (-not $mappedLog) { + New-EventLog -LogName $EventLog -Source $EventSource -ErrorAction Stop + } elseif ($mappedLog -ne $EventLog) { + # Source already exists but mapped to a different log. Create a new source mapped to target log. + $newSource = "$EventSource - $EventLog" + if (-not [System.Diagnostics.EventLog]::SourceExists($newSource)) { + New-EventLog -LogName $EventLog -Source $newSource -ErrorAction Stop + } + $EventSource = $newSource + } + } catch { + Write-Host "[Warning] Could not ensure event log/source ($EventLog / $EventSource): $($_.Exception.Message)" -ForegroundColor Yellow + # continue; we'll still try to write + } + + $entryType = if ($Level -in 'Warning','Error') { $Level } else { 'Information' } + try { + Write-EventLog -LogName $EventLog -Source $EventSource -EntryType $entryType -EventID $EventID -Message $fmt + } catch { + Write-Host "[Warning] EventLog write failed: $($_.Exception.Message)" -ForegroundColor Yellow + } + } + + if ($PassThru) { return $Global:LogCache[-1] } } - if ($PassThru) { return $Global:LogCache[-1] } - } + function Write-LogHybrid { + [CmdletBinding()] + param( + [Parameter(Mandatory=$true)][string]$Message, + [ValidateSet("Info","Warning","Error","Success","General")] + [string]$Level = "Info", + [string]$TaskCategory = "GeneralTask", + [switch]$LogToEvent, + [string]$EventSource = "Script Automation Monkey", + [string]$EventLog = "SVS Scripting", # <— default changed from Application + [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] + [string]$ForegroundColorOverride + ) - function Write-LogHybrid { - [CmdletBinding()] - param( - [Parameter(Mandatory=$true)][string]$Message, - [ValidateSet("Info","Warning","Error","Success","General")] - [string]$Level = "Info", - [string]$TaskCategory = "GeneralTask", - [switch]$LogToEvent, - [string]$EventSource = "Script Automation Monkey", - [string]$EventLog = "SVS Scripting", - [ValidateSet("Black","DarkGray","Gray","White","Red","Green","Blue","Yellow","Magenta","Cyan")] - [string]$ForegroundColorOverride - ) + $formatted = "[$Level] [$TaskCategory] $Message" - $formatted = "[$Level] [$TaskCategory] $Message" - - if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { - Write-Host $formatted -ForegroundColor $ForegroundColorOverride - $invokeParams = @{ - Message = $Message - Level = $Level - TaskCategory = $TaskCategory - LogToEvent = $LogToEvent - EventSource = $EventSource - EventLog = $EventLog - } - if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } else { Write-LogHelper @invokeParams } - } else { - if (Get-Command Write-Log -ErrorAction SilentlyContinue) { - Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog + if ($PSBoundParameters.ContainsKey('ForegroundColorOverride')) { + Write-Host $formatted -ForegroundColor $ForegroundColorOverride + $invokeParams = @{ + Message = $Message + Level = $Level + TaskCategory = $TaskCategory + LogToEvent = $LogToEvent + EventSource = $EventSource + EventLog = $EventLog + } + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { Write-Log @invokeParams } else { Write-LogHelper @invokeParams } } else { - Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog + if (Get-Command Write-Log -ErrorAction SilentlyContinue) { + Write-Log -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog + } else { + Write-LogHelper -Message $Message -Level $Level -TaskCategory $TaskCategory -LogToEvent:$LogToEvent -EventSource $EventSource -EventLog $EventLog + } } } - } + #endregion Write-Log + + #region building the Menus + $Global:Tasks = @( + # On-Boarding, left column + @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-setSVSPowerPlan'; Page='onboard'; Column='left' }, + @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' }, + @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' }, + @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' }, + @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' }, + @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' }, + @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left'; + SubOptions= @( + @{ Value='inputVar'; Label='Copy Site Variables' }, + @{ Value='rmm'; Label='Install RMM Agent' }, + @{ Value='exe'; Label='Download Executable' } + ) + }, - #endregion Write-Log + # On-Boarding, right column (optional bits) + @{ 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='set-EdgeDefaultSearchProvider'; Page='onboard'; Column='right' }, - #region Tasks registry + # Off-Boarding + @{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' }, + @{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' }, - $Global:Tasks = @( - # On-Boarding, left column - @{ Id='setSVSPowerplan'; Name='setSVSPowerplan'; Label='Set SVS Powerplan'; HandlerFn='Handle-SetSVSPowerPlan'; Page='onboard'; Column='left' }, - @{ Id='installSVSMSPModule'; Name='installSVSMSPModule'; Label='Install SVSMSP Module'; HandlerFn='Handle-InstallSVSMSP'; Page='onboard'; Column='left' }, - @{ Id='installCyberQP'; Name='installCyberQP'; Label='Install CyberQP'; HandlerFn='Handle-InstallCyberQP'; Page='onboard'; Column='left' }, - @{ Id='installSVSHelpDesk'; Name='installSVSHelpDesk'; Label='Install SVS HelpDesk'; HandlerFn='Handle-InstallSVSHelpDesk'; Page='onboard'; Column='left' }, - @{ Id='installThreatLocker'; Name='installThreatLocker'; Label='Install ThreatLocker'; HandlerFn='Handle-InstallThreatLocker'; Page='onboard'; Column='left' }, - @{ Id='installRocketCyber'; Name='installRocketCyber'; Label='Install RocketCyber'; HandlerFn='Handle-InstallRocketCyber'; Page='onboard'; Column='left' }, - @{ Id='installDattoRMM'; Name='installDattoRMM'; Label='Install DattoRMM'; HandlerFn='Handle-InstallDattoRMM'; Page='onboard'; Column='left'; - SubOptions= @( - @{ Value='inputVar'; Label='Copy Site Variables' }, - @{ Value='rmm'; Label='Install RMM Agent' }, - @{ Value='exe'; Label='Download Executable' } - ) - }, + # Tweaks + @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' }, - # On-Boarding, right column (optional bits) - @{ 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='set-EdgeDefaultSearchProvider'; Page='onboard'; Column='right' }, + # SVS Apps + @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }, + @{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' }, + @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' } + ) + #endregion building the Menus - # Off-Boarding - @{ Id='uninstallCyberQP'; Name='uninstallCyberQP'; Label='Uninstall CyberQP'; HandlerFn='Uninstall-CyberQP'; Page='offboard' }, - @{ Id='uninstallSVSMSPModule'; Name='uninstallSVSMSPModule'; Label='Uninstall SVSMSP Module'; HandlerFn='Cleanup-SVSMSP'; Page='offboard' }, + #region Build-Checkboxes + function Build-Checkboxes { + param($Page, $Column) - # Tweaks - @{ Id='disableAnimations'; Name='disableAnimations'; Label='Disable Animations'; HandlerFn='Disable-Animations'; Page='tweaks' }, + ( + $Global:Tasks | + Where-Object Page -EQ $Page | + Where-Object Column -EQ $Column | + ForEach-Object { + $taskId = $_.Id + $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) { " title='$($_.Tooltip)'" } else { '' } - # SVS Apps - @{ Id='wingetLastpass'; Name='wingetLastpass'; Label='LastPass Desktop App'; HandlerFn='Install-WingetLastPass'; Page='SVSApps' }, - @{ Id='wingetChrome'; Name='wingetChrome'; Label='Google Chrome'; HandlerFn='Handle-InstallChrome'; Page='SVSApps' }, - @{ Id='wingetAcrobat'; Name='wingetAcrobat'; Label='Adobe Acrobat Reader (64-bit)'; HandlerFn='Handle-InstallAcrobat'; Page='SVSApps' } - ) + $html = " $($_.Label)" - #endregion Tasks registry - - #region Build-Checkboxes - - function Build-Checkboxes { - param($Page, $Column) - - ( - $Global:Tasks | - Where-Object Page -EQ $Page | - Where-Object Column -EQ $Column | - ForEach-Object { - $taskId = $_.Id - $tooltip = if ($_.PSObject.Properties.Name -contains 'Tooltip' -and $_.Tooltip) { - " title='$($_.Tooltip)'" - } else { '' } - - $html = " $($_.Label)" - - if ($_.SubOptions) { - $subHtml = ( - $_.SubOptions | - ForEach-Object { + if ($_.SubOptions) { + $subHtml = ( + $_.SubOptions | ForEach-Object { "" - } - ) -join "`n" + } + ) -join "`n" - $html += @" + $html += @" "@ + } + $html } - $html - } - ) -join "`n" + ) -join "`n" + } + #endregion Build-Checkboxes + + #region Get-ModuleVersionHtml + function Get-ModuleVersionHtml { + $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1 + if ($mod) { return "
Module Version: $($mod.Version)
" } + return "
SVSMSP_Module not found
" + } + #endregion Get-ModuleVersionHtml + + #region Strat-Server + function Get-NextFreePort { + param([int]$Start = $Port) + 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." } - #endregion Build-Checkboxes - - #region Get-ModuleVersionHtml - function Get-ModuleVersionHtml { - $mod = Get-Module -ListAvailable -Name SVSMSP | Sort-Object Version -Descending | Select-Object -First 1 - if ($mod) { - return "
Module Version: $($mod.Version)
" - } - return "
SVSMSP_Module not found
" - } - #endregion Get-ModuleVersionHtml - - #region HTTP prereqs (URL ACL + port check) - - function Ensure-HttpPrereqs { - param( - [int]$Port, - [string[]]$Prefixes - ) - - # Port already bound? - try { - $inUse = Get-NetTCPConnection -LocalPort $Port -State Listen -ErrorAction Stop - } catch { $inUse = $null } - if ($inUse) { - $pids = ($inUse | Select-Object -ExpandProperty OwningProcess -Unique) -join ',' - throw "Port $Port is already in use (PID(s): $pids). Choose another port or stop the process." - } - - # Ensure URL ACLs - $current = (& netsh http show urlacl) 2>$null - foreach ($p in $Prefixes) { - $pfx = if ($p.EndsWith('/')) { $p } else { "$p/" } - if ($null -eq $current -or ($current -join "`n") -notmatch [regex]::Escape($pfx)) { - $user = "$env:USERDOMAIN\$env:USERNAME" - $args = "http add urlacl url=$pfx user=""$user"" listen=yes" - $psi = New-Object System.Diagnostics.ProcessStartInfo -Property @{ - FileName = 'netsh.exe' - Arguments = $args - Verb = 'runas' - UseShellExecute = $true - WindowStyle = 'Hidden' - } - $proc = [System.Diagnostics.Process]::Start($psi); $proc.WaitForExit() - if ($proc.ExitCode -ne 0) { - throw "Failed to add URL ACL for $pfx (netsh exit $($proc.ExitCode))." - } else { - Write-LogHybrid "Registered URL ACL for $pfx" "Success" "Server" -LogToEvent - } - } - } - } - - #endregion HTTP prereqs - - #region Start-Server (hardened for 25H2) - + # Starts the HTTP listener loop function Start-Server { - $prefixes = @("http://localhost:$Port/","http://127.0.0.1:$Port/") - try { - Ensure-HttpPrereqs -Port $Port -Prefixes $prefixes - } catch { - Write-LogHybrid "HTTP prereq check failed: $($_.Exception.Message)" "Error" "Server" -LogToEvent - throw - } - $Global:Listener = [System.Net.HttpListener]::new() - foreach ($p in $prefixes) { $Global:Listener.Prefixes.Add($p) } + $primaryPrefix = "http://localhost:$Port/" + $wildcardPrefix = "http://+:$Port/" try { + $Global:Listener.Prefixes.Add($primaryPrefix) $Global:Listener.Start() - Write-LogHybrid "Listening on $($prefixes -join ', ')" "Info" "Server" -LogToEvent - } catch { - Write-LogHybrid "HttpListener failed to start: $($_.Exception.Message)" "Error" "Server" -LogToEvent - if ($_.Exception.InnerException) { - Write-LogHybrid "Inner: $($_.Exception.InnerException.Message)" "Error" "Server" -LogToEvent + 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 } - throw } try { @@ -444,23 +419,21 @@ $subHtml try { Dispatch-Request $ctx } catch { - Write-LogHybrid "Dispatch error: $($_.Exception.Message)" "Error" "Server" -LogToEvent - $ctx.Response.StatusCode = 500 - Respond-Text $ctx "Internal server error." + Write-LogHybrid "Dispatch error: $($_.Exception.Message)" Error Server -LogToEvent } } - } finally { + } + finally { $Global:Listener.Close() - Write-LogHybrid "Listener closed." "Info" "Server" -LogToEvent + Write-LogHybrid "Listener closed." Info Server -LogToEvent } } +#endregion Strat-Server - #endregion Start-Server - #region UI HTML/JS - - function Get-UIHtml { - param([string]$Page = 'onboard') + #region UIHtml + function Get-UIHtml { + param([string]$Page = 'onboard') $style = @'