Files
claudetools/projects/msp-tools/guru-rmm/agent-legacy/GuruRMM-Agent.ps1
azcomputerguru 65086f4407 fix(security): Implement Phase 1 critical security fixes
CORS:
- Restrict CORS to DASHBOARD_URL environment variable
- Default to production dashboard domain

Authentication:
- Add AuthUser requirement to all agent management endpoints
- Add AuthUser requirement to all command endpoints
- Add AuthUser requirement to all metrics endpoints
- Add audit logging for command execution (user_id tracked)

Agent Security:
- Replace Unicode characters with ASCII markers [OK]/[ERROR]/[WARNING]
- Add certificate pinning for update downloads (allowlist domains)
- Fix insecure temp file creation (use /var/run/gururmm with 0700 perms)
- Fix rollback script backgrounding (use setsid instead of literal &)

Dashboard Security:
- Move token storage from localStorage to sessionStorage
- Add proper TypeScript types (remove 'any' from error handlers)
- Centralize token management functions

Legacy Agent:
- Add -AllowInsecureTLS parameter (opt-in required)
- Add Windows Event Log audit trail when insecure mode used
- Update documentation with security warnings

Closes: Phase 1 items in issue #1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-20 21:16:24 -07:00

589 lines
19 KiB
PowerShell

#Requires -Version 2.0
<#
.SYNOPSIS
GuruRMM Legacy Agent for Windows Server 2008 R2 and older systems.
.DESCRIPTION
This PowerShell-based agent is designed for legacy Windows systems that cannot
run the modern Rust-based GuruRMM agent. It provides basic RMM functionality
including registration, heartbeat, system info collection, and remote command
execution.
IMPORTANT: This agent is intended for legacy systems only. For Windows 10/
Server 2016 and newer, use the native Rust agent instead.
.PARAMETER ConfigPath
Path to the agent configuration file. Default: $env:ProgramData\GuruRMM\agent.json
.PARAMETER ServerUrl
The URL of the GuruRMM server (e.g., https://rmm.example.com)
.PARAMETER SiteCode
The site code for agent registration (e.g., ACME-CORP-1234)
.PARAMETER AllowInsecureTLS
[SECURITY RISK] Disables SSL/TLS certificate validation. Required ONLY for
systems with self-signed certificates or broken certificate chains.
WARNING: This flag makes the connection vulnerable to man-in-the-middle
attacks. Only use on isolated networks or when absolutely necessary.
This flag must be explicitly provided - certificate validation is enabled
by default.
.PARAMETER Register
Register this agent with the server.
.EXAMPLE
# Secure installation (recommended)
.\GuruRMM-Agent.ps1 -Register -ServerUrl "https://rmm.example.com" -SiteCode "ACME-CORP-1234"
.EXAMPLE
# Insecure installation (legacy systems with self-signed certs ONLY)
.\GuruRMM-Agent.ps1 -Register -ServerUrl "https://rmm.example.com" -SiteCode "ACME-CORP-1234" -AllowInsecureTLS
.EXAMPLE
# Run the agent
.\GuruRMM-Agent.ps1
.NOTES
Version: 1.1.0
Requires: PowerShell 2.0+
Platforms: Windows Server 2008 R2, Windows 7, and newer
Author: GuruRMM
#>
param(
[Parameter()]
[string]$ConfigPath = "$env:ProgramData\GuruRMM\agent.json",
[Parameter()]
[switch]$Register,
[Parameter()]
[string]$SiteCode,
[Parameter()]
[string]$ServerUrl = "https://rmm-api.azcomputerguru.com",
[Parameter()]
[switch]$AllowInsecureTLS
)
# ============================================================================
# Configuration
# ============================================================================
$script:Version = "1.1.0"
$script:AgentType = "powershell-legacy"
$script:ConfigDir = "$env:ProgramData\GuruRMM"
$script:LogFile = "$script:ConfigDir\agent.log"
$script:PollInterval = 60 # seconds
$script:AllowInsecureTLS = $AllowInsecureTLS
$script:TLSInitialized = $false
# ============================================================================
# Logging
# ============================================================================
function Write-Log {
param([string]$Message, [string]$Level = "INFO")
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logLine = "[$timestamp] [$Level] $Message"
# Write to console
switch ($Level) {
"ERROR" { Write-Host $logLine -ForegroundColor Red }
"WARN" { Write-Host $logLine -ForegroundColor Yellow }
"DEBUG" { Write-Host $logLine -ForegroundColor Gray }
default { Write-Host $logLine }
}
# Write to file
try {
if (-not (Test-Path $script:ConfigDir)) {
New-Item -ItemType Directory -Path $script:ConfigDir -Force | Out-Null
}
Add-Content -Path $script:LogFile -Value $logLine -ErrorAction SilentlyContinue
} catch {}
}
# ============================================================================
# TLS Initialization
# ============================================================================
function Initialize-TLS {
if ($script:TLSInitialized) {
return
}
# Configure TLS - prefer TLS 1.2
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
Write-Log "TLS 1.2 configured successfully" "INFO"
} catch {
Write-Log "TLS 1.2 not available, trying TLS 1.1" "WARN"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls11
} catch {
Write-Log "TLS 1.1 not available - using system default TLS" "WARN"
try {
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls
} catch {
Write-Log "TLS configuration failed - connection security may be limited" "WARN"
}
}
}
# Certificate validation - ONLY disable if explicitly requested
if ($script:AllowInsecureTLS) {
Write-Log "============================================" "WARN"
Write-Log "[SECURITY WARNING] Certificate validation DISABLED" "WARN"
Write-Log "This makes the connection vulnerable to MITM attacks" "WARN"
Write-Log "Only use on legacy systems with self-signed certificates" "WARN"
Write-Log "============================================" "WARN"
# Log to Windows Event Log for audit trail
try {
$source = "GuruRMM"
if (-not [System.Diagnostics.EventLog]::SourceExists($source)) {
New-EventLog -LogName Application -Source $source -ErrorAction SilentlyContinue
}
Write-EventLog -LogName Application -Source $source -EventId 1001 -EntryType Warning `
-Message "GuruRMM agent started with certificate validation disabled (-AllowInsecureTLS). This is a security risk."
} catch {
Write-Log "Could not write to Windows Event Log: $_" "WARN"
}
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
} else {
Write-Log "Certificate validation ENABLED (secure mode)" "INFO"
# Ensure callback is reset to default (validate certificates)
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null
}
$script:TLSInitialized = $true
}
# ============================================================================
# HTTP Functions (PS 2.0 compatible)
# ============================================================================
function Invoke-ApiRequest {
param(
[string]$Endpoint,
[string]$Method = "GET",
[hashtable]$Body,
[string]$ApiKey
)
$url = "$($script:Config.ServerUrl)$Endpoint"
try {
# Initialize TLS settings (only runs once)
Initialize-TLS
# Use .NET WebClient for PS 2.0 compatibility
$webClient = New-Object System.Net.WebClient
$webClient.Headers.Add("Content-Type", "application/json")
$webClient.Headers.Add("User-Agent", "GuruRMM-Legacy/$script:Version")
if ($ApiKey) {
$webClient.Headers.Add("Authorization", "Bearer $ApiKey")
}
if ($Method -eq "GET") {
$response = $webClient.DownloadString($url)
} else {
$jsonBody = ConvertTo-JsonCompat $Body
$response = $webClient.UploadString($url, $Method, $jsonBody)
}
return ConvertFrom-JsonCompat $response
} catch [System.Net.WebException] {
$statusCode = $null
if ($_.Exception.Response) {
$statusCode = [int]$_.Exception.Response.StatusCode
}
Write-Log "API request failed: $($_.Exception.Message) (Status: $statusCode)" "ERROR"
return $null
} catch {
Write-Log "API request error: $($_.Exception.Message)" "ERROR"
return $null
}
}
# PS 2.0 compatible JSON functions
function ConvertTo-JsonCompat {
param([object]$Object)
if (Get-Command ConvertTo-Json -ErrorAction SilentlyContinue) {
return ConvertTo-Json $Object -Depth 10
}
# Manual JSON serialization for PS 2.0
$serializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer
return $serializer.Serialize($Object)
}
function ConvertFrom-JsonCompat {
param([string]$Json)
if (-not $Json) { return $null }
if (Get-Command ConvertFrom-Json -ErrorAction SilentlyContinue) {
return ConvertFrom-Json $Json
}
# Manual JSON deserialization for PS 2.0
Add-Type -AssemblyName System.Web.Extensions
$serializer = New-Object System.Web.Script.Serialization.JavaScriptSerializer
return $serializer.DeserializeObject($Json)
}
# ============================================================================
# Configuration Management
# ============================================================================
function Get-AgentConfig {
if (Test-Path $ConfigPath) {
try {
$content = Get-Content $ConfigPath -Raw
return ConvertFrom-JsonCompat $content
} catch {
Write-Log "Failed to read config: $($_.Exception.Message)" "ERROR"
}
}
return $null
}
function Save-AgentConfig {
param([hashtable]$Config)
try {
if (-not (Test-Path $script:ConfigDir)) {
New-Item -ItemType Directory -Path $script:ConfigDir -Force | Out-Null
}
$json = ConvertTo-JsonCompat $Config
Set-Content -Path $ConfigPath -Value $json -Force
Write-Log "Configuration saved to $ConfigPath"
return $true
} catch {
Write-Log "Failed to save config: $($_.Exception.Message)" "ERROR"
return $false
}
}
# ============================================================================
# System Information Collection
# ============================================================================
function Get-SystemInfo {
$info = @{}
try {
# Basic info
$os = Get-WmiObject Win32_OperatingSystem
$cs = Get-WmiObject Win32_ComputerSystem
$cpu = Get-WmiObject Win32_Processor | Select-Object -First 1
$info.hostname = $env:COMPUTERNAME
$info.os_type = "Windows"
$info.os_version = $os.Caption
$info.os_build = $os.BuildNumber
$info.architecture = $os.OSArchitecture
# Uptime
$bootTime = $os.ConvertToDateTime($os.LastBootUpTime)
$uptime = (Get-Date) - $bootTime
$info.uptime_seconds = [int]$uptime.TotalSeconds
$info.last_boot = $bootTime.ToString("yyyy-MM-ddTHH:mm:ssZ")
# Memory
$info.memory_total_mb = [math]::Round($cs.TotalPhysicalMemory / 1MB)
$info.memory_free_mb = [math]::Round($os.FreePhysicalMemory / 1KB)
$info.memory_used_percent = [math]::Round((1 - ($os.FreePhysicalMemory * 1KB / $cs.TotalPhysicalMemory)) * 100, 1)
# CPU
$info.cpu_name = $cpu.Name.Trim()
$info.cpu_cores = $cpu.NumberOfCores
$info.cpu_logical = $cpu.NumberOfLogicalProcessors
$info.cpu_usage_percent = (Get-WmiObject Win32_Processor | Measure-Object -Property LoadPercentage -Average).Average
# Disk
$disks = @()
Get-WmiObject Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object {
$disks += @{
drive = $_.DeviceID
total_gb = [math]::Round($_.Size / 1GB, 1)
free_gb = [math]::Round($_.FreeSpace / 1GB, 1)
used_percent = [math]::Round((1 - ($_.FreeSpace / $_.Size)) * 100, 1)
}
}
$info.disks = $disks
# Network
$adapters = @()
Get-WmiObject Win32_NetworkAdapterConfiguration -Filter "IPEnabled=True" | ForEach-Object {
$adapters += @{
name = $_.Description
ip_addresses = @($_.IPAddress | Where-Object { $_ })
mac_address = $_.MACAddress
}
}
$info.network_adapters = $adapters
# Get primary IP
$primaryIp = (Get-WmiObject Win32_NetworkAdapterConfiguration |
Where-Object { $_.IPAddress -and $_.DefaultIPGateway } |
Select-Object -First 1).IPAddress |
Where-Object { $_ -match '^\d+\.\d+\.\d+\.\d+$' } |
Select-Object -First 1
$info.primary_ip = $primaryIp
# Agent info
$info.agent_version = $script:Version
$info.agent_type = $script:AgentType
$info.powershell_version = $PSVersionTable.PSVersion.ToString()
$info.timestamp = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
} catch {
Write-Log "Error collecting system info: $($_.Exception.Message)" "ERROR"
}
return $info
}
# ============================================================================
# Registration
# ============================================================================
function Register-Agent {
param([string]$SiteCode)
if (-not $SiteCode) {
# Prompt for site code
Write-Host ""
Write-Host "=== GuruRMM Legacy Agent Registration ===" -ForegroundColor Cyan
Write-Host ""
$SiteCode = Read-Host "Enter site code (WORD-WORD-NUMBER)"
}
# Validate format
if ($SiteCode -notmatch '^[A-Z]+-[A-Z]+-\d+$') {
$SiteCode = $SiteCode.ToUpper()
if ($SiteCode -notmatch '^[A-Z]+-[A-Z]+-\d+$') {
Write-Log "Invalid site code format. Expected: WORD-WORD-NUMBER (e.g., DARK-GROVE-7839)" "ERROR"
return $false
}
}
Write-Log "Registering with site code: $SiteCode"
# Collect system info for registration
$sysInfo = Get-SystemInfo
$regData = @{
site_code = $SiteCode
hostname = $sysInfo.hostname
os_type = $sysInfo.os_type
os_version = $sysInfo.os_version
agent_version = $script:Version
agent_type = $script:AgentType
}
# Call registration endpoint
$script:Config = @{ ServerUrl = $ServerUrl }
$result = Invoke-ApiRequest -Endpoint "/api/agent/register-legacy" -Method "POST" -Body $regData
if ($result -and $result.api_key) {
# Save configuration
$config = @{
ServerUrl = $ServerUrl
ApiKey = $result.api_key
AgentId = $result.agent_id
SiteCode = $SiteCode
RegisteredAt = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
}
if (Save-AgentConfig $config) {
Write-Host ""
Write-Host "Registration successful!" -ForegroundColor Green
Write-Host " Agent ID: $($result.agent_id)" -ForegroundColor Cyan
Write-Host " Site: $($result.site_name)" -ForegroundColor Cyan
Write-Host ""
return $true
}
} else {
Write-Log "Registration failed. Check site code and server connectivity." "ERROR"
}
return $false
}
# ============================================================================
# Heartbeat / Check-in
# ============================================================================
function Send-Heartbeat {
$sysInfo = Get-SystemInfo
$heartbeat = @{
agent_id = $script:Config.AgentId
timestamp = $sysInfo.timestamp
system_info = $sysInfo
}
$result = Invoke-ApiRequest -Endpoint "/api/agent/heartbeat" -Method "POST" -Body $heartbeat -ApiKey $script:Config.ApiKey
if ($result) {
Write-Log "Heartbeat sent successfully" "DEBUG"
# Check for pending commands
if ($result.pending_commands -and $result.pending_commands.Count -gt 0) {
foreach ($cmd in $result.pending_commands) {
Execute-RemoteCommand $cmd
}
}
return $true
}
return $false
}
# ============================================================================
# Remote Command Execution
# ============================================================================
function Execute-RemoteCommand {
param([hashtable]$Command)
Write-Log "Executing command: $($Command.id) - $($Command.type)"
$result = @{
command_id = $Command.id
started_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
success = $false
output = ""
error = ""
}
try {
switch ($Command.type) {
"powershell" {
# Execute PowerShell script
$output = Invoke-Expression $Command.script 2>&1
$result.output = $output | Out-String
$result.success = $true
}
"cmd" {
# Execute CMD command
$output = cmd /c $Command.script 2>&1
$result.output = $output | Out-String
$result.success = $true
}
"info" {
# Return system info
$result.output = ConvertTo-JsonCompat (Get-SystemInfo)
$result.success = $true
}
default {
$result.error = "Unknown command type: $($Command.type)"
}
}
} catch {
$result.error = $_.Exception.Message
$result.output = $_.Exception.ToString()
}
$result.completed_at = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
# Report result back
Invoke-ApiRequest -Endpoint "/api/agent/command-result" -Method "POST" -Body $result -ApiKey $script:Config.ApiKey | Out-Null
Write-Log "Command $($Command.id) completed. Success: $($result.success)"
}
# ============================================================================
# Main Agent Loop
# ============================================================================
function Start-AgentLoop {
Write-Log "Starting GuruRMM Legacy Agent v$script:Version"
Write-Log "Server: $($script:Config.ServerUrl)"
Write-Log "Agent ID: $($script:Config.AgentId)"
Write-Log "Poll interval: $script:PollInterval seconds"
$consecutiveFailures = 0
$maxFailures = 5
while ($true) {
try {
if (Send-Heartbeat) {
$consecutiveFailures = 0
} else {
$consecutiveFailures++
Write-Log "Heartbeat failed ($consecutiveFailures/$maxFailures)" "WARN"
}
# Back off if too many failures
if ($consecutiveFailures -ge $maxFailures) {
$backoffSeconds = [math]::Min(300, $script:PollInterval * $consecutiveFailures)
Write-Log "Too many failures, backing off for $backoffSeconds seconds" "WARN"
Start-Sleep -Seconds $backoffSeconds
} else {
Start-Sleep -Seconds $script:PollInterval
}
} catch {
Write-Log "Agent loop error: $($_.Exception.Message)" "ERROR"
Start-Sleep -Seconds $script:PollInterval
}
}
}
# ============================================================================
# Entry Point
# ============================================================================
# Load System.Web.Extensions for JSON (PS 2.0)
try {
Add-Type -AssemblyName System.Web.Extensions -ErrorAction SilentlyContinue
} catch {}
# Check if registering
if ($Register -or $SiteCode) {
if (Register-Agent -SiteCode $SiteCode) {
Write-Host "Run the agent with: .\GuruRMM-Agent.ps1" -ForegroundColor Yellow
}
exit
}
# Load config
$script:Config = Get-AgentConfig
if (-not $script:Config -or -not $script:Config.ApiKey) {
Write-Host ""
Write-Host "GuruRMM Legacy Agent is not registered." -ForegroundColor Yellow
Write-Host ""
Write-Host "To register, run:" -ForegroundColor Cyan
Write-Host " .\GuruRMM-Agent.ps1 -Register" -ForegroundColor White
Write-Host ""
Write-Host "Or with site code:" -ForegroundColor Cyan
Write-Host " .\GuruRMM-Agent.ps1 -SiteCode DARK-GROVE-7839" -ForegroundColor White
Write-Host ""
exit 1
}
# Override server URL if provided
if ($ServerUrl -and $ServerUrl -ne "https://rmm-api.azcomputerguru.com") {
$script:Config.ServerUrl = $ServerUrl
}
# Start the agent
Start-AgentLoop