#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