# GuruRMM Remediation Plan **Generated:** 2026-01-20 **Review Source:** Full-scale code review of all components **Priority:** Critical issues first, then major, then warnings --- ## Executive Summary This document provides specific code fixes for all critical and major issues identified during the comprehensive code review. Issues are organized by component and priority. **Issue Counts:** - CRITICAL: 16 (must fix before production) - MAJOR: 14 (should fix before production) - WARNING: 28 (recommended improvements) --- ## Phase 1: Critical Security Fixes (MUST DO) ### 1.1 Server: Restrict CORS Configuration **File:** `server/src/main.rs` **Lines:** 133-136 **Severity:** CRITICAL **Current Code:** ```rust let cors = CorsLayer::new() .allow_origin(Any) .allow_methods(Any) .allow_headers(Any); ``` **Fixed Code:** ```rust use tower_http::cors::{CorsLayer, AllowOrigin}; use http::HeaderValue; // Get allowed origins from environment let dashboard_origin = std::env::var("DASHBOARD_URL") .unwrap_or_else(|_| "https://rmm.azcomputerguru.com".to_string()); let cors = CorsLayer::new() .allow_origin(AllowOrigin::exact( HeaderValue::from_str(&dashboard_origin).expect("Invalid DASHBOARD_URL") )) .allow_methods([ http::Method::GET, http::Method::POST, http::Method::PUT, http::Method::DELETE, http::Method::OPTIONS, ]) .allow_headers([ http::header::AUTHORIZATION, http::header::CONTENT_TYPE, http::header::ACCEPT, ]) .allow_credentials(true); ``` **Environment Variable:** ```bash DASHBOARD_URL=https://rmm.azcomputerguru.com ``` --- ### 1.2 Server: Add Authentication to All Endpoints **File:** `server/src/api/agents.rs` **Severity:** CRITICAL **Problem:** Most endpoints lack the `AuthUser` extractor. **Fix Pattern - Apply to ALL endpoint functions:** ```rust // BEFORE (insecure) pub async fn list_agents( State(state): State, ) -> Result>, (StatusCode, String)> { // AFTER (secure) pub async fn list_agents( State(state): State, _user: AuthUser, // Add this to require authentication ) -> Result>, (StatusCode, String)> { ``` **Endpoints requiring AuthUser addition:** **agents.rs:** - `list_agents` (line 62) - `get_agent` (line 74) - `delete_agent` (line 87) - `get_stats` (line 109) - `move_agent` (line 126) - `list_agents_with_details` (line 152) - `list_unassigned_agents` (line 163) - `get_agent_state` (line 175) - `register_agent` (line 32) - Add auth OR implement rate limiting **commands.rs:** - `send_command` (line 46) - `list_commands` (line 103) - `get_command` (line 117) **metrics.rs:** - `get_agent_metrics` (line 29) - `get_summary` (line 57) **Complete Fix for commands.rs:** ```rust // server/src/api/commands.rs use crate::auth::AuthUser; pub async fn send_command( State(state): State, user: AuthUser, // ADD THIS Path(agent_id): Path, Json(req): Json, ) -> Result, (StatusCode, String)> { // Log who sent the command tracing::info!( user_id = %user.user_id, agent_id = %agent_id, command_type = ?req.command_type, "Command sent by user" ); // Create command with user attribution let command = db::create_command( &state.db, agent_id, &req.command_type, &req.command, req.timeout_seconds, req.elevated.unwrap_or(false), Some(user.user_id), // Track who created it ) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; // ... rest of function } pub async fn list_commands( State(state): State, _user: AuthUser, // ADD THIS ) -> Result>, (StatusCode, String)> { // ... existing code } pub async fn get_command( State(state): State, _user: AuthUser, // ADD THIS Path(command_id): Path, ) -> Result, (StatusCode, String)> { // ... existing code } ``` --- ### 1.3 Server: Add Rate Limiting to Registration **File:** `server/src/main.rs` **Severity:** CRITICAL **Add dependency to Cargo.toml:** ```toml tower-governor = "0.3" ``` **Implementation:** ```rust // server/src/main.rs use tower_governor::{GovernorLayer, GovernorConfigBuilder, KeyExtractor}; use std::net::IpAddr; // Custom key extractor for rate limiting by IP #[derive(Clone)] struct RealIpKeyExtractor; impl KeyExtractor for RealIpKeyExtractor { type Key = IpAddr; fn extract(&self, req: &http::Request) -> Result { // Try X-Forwarded-For first (for reverse proxy) if let Some(forwarded) = req.headers().get("x-forwarded-for") { if let Ok(s) = forwarded.to_str() { if let Some(ip) = s.split(',').next() { if let Ok(addr) = ip.trim().parse() { return Ok(addr); } } } } // Fall back to connection info req.extensions() .get::>() .map(|ci| ci.0.ip()) .ok_or(GovernorError::UnableToExtractKey) } } // In main() or router setup: let registration_limiter = GovernorConfigBuilder::default() .per_second(1) // 1 request per second .burst_size(5) // Allow burst of 5 .key_extractor(RealIpKeyExtractor) .finish() .unwrap(); let app = Router::new() // Rate-limited registration endpoints .route("/api/agents/register", post(register_agent).layer(GovernorLayer::new(®istration_limiter))) .route("/api/agents/register-legacy", post(register_legacy).layer(GovernorLayer::new(®istration_limiter))) // ... other routes without rate limiting ``` --- ### 1.4 Agent: Remove Unicode Characters **File:** `agent/src/main.rs` **Lines:** 460, 467, 478, 480 **Severity:** CRITICAL **Find and replace all Unicode characters:** ```rust // BEFORE println!("\n✓ GuruRMM Agent installed successfully!"); println!("\n⚠️ IMPORTANT: Edit {} with your server URL and API key!", config_dest); // AFTER println!("\n[OK] GuruRMM Agent installed successfully!"); println!("\n[WARNING] IMPORTANT: Edit {} with your server URL and API key!", config_dest); ``` **Complete list of replacements:** | Before | After | |--------|-------| | `✓` | `[OK]` | | `✗` | `[ERROR]` | | `⚠️` | `[WARNING]` | | `ℹ️` | `[INFO]` | | `→` | `->` | | `•` | `-` | --- ### 1.5 Agent: Secure Update Download with Certificate Pinning **File:** `agent/src/updater/mod.rs` **Severity:** CRITICAL **Add to Cargo.toml:** ```toml [dependencies] sha2 = "0.10" ``` **Implementation:** ```rust // agent/src/updater/mod.rs use sha2::{Sha256, Digest}; impl Updater { /// Download binary with certificate validation async fn download_binary(&self, url: &str, expected_checksum: &str) -> Result { // Validate URL is from trusted domain let allowed_domains = [ "rmm-api.azcomputerguru.com", "downloads.azcomputerguru.com", ]; let parsed_url = url::Url::parse(url) .context("Invalid download URL")?; let host = parsed_url.host_str() .ok_or_else(|| anyhow::anyhow!("No host in download URL"))?; if !allowed_domains.iter().any(|d| host == *d || host.ends_with(&format!(".{}", d))) { return Err(anyhow::anyhow!( "Download URL host '{}' not in allowed domains: {:?}", host, allowed_domains )); } // Require HTTPS if parsed_url.scheme() != "https" { return Err(anyhow::anyhow!("Download URL must use HTTPS")); } info!("Downloading update from: {}", url); let response = self.http_client .get(url) .send() .await .context("HTTP request failed")?; if !response.status().is_success() { return Err(anyhow::anyhow!( "Download failed with status: {}", response.status() )); } let bytes = response.bytes().await .context("Failed to read response body")?; // Verify checksum BEFORE writing to disk let mut hasher = Sha256::new(); hasher.update(&bytes); let actual_checksum = format!("{:x}", hasher.finalize()); if actual_checksum != expected_checksum { return Err(anyhow::anyhow!( "Checksum mismatch! Expected: {}, Got: {}", expected_checksum, actual_checksum )); } info!("Checksum verified: {}", actual_checksum); // Write to secure temp location let temp_dir = std::env::temp_dir(); let temp_path = temp_dir.join(format!("gururmm-update-{}.tmp", uuid::Uuid::new_v4())); tokio::fs::write(&temp_path, &bytes).await .context("Failed to write update binary")?; // Set restrictive permissions on Unix #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let perms = std::fs::Permissions::from_mode(0o700); std::fs::set_permissions(&temp_path, perms)?; } Ok(temp_path) } } ``` --- ### 1.6 Agent: Fix Insecure Temp File Creation **File:** `agent/src/updater/mod.rs` **Lines:** 275-331 **Severity:** CRITICAL **Problem:** Using world-writable `/tmp/` for rollback script. **Fixed Code:** ```rust // agent/src/updater/mod.rs #[cfg(unix)] async fn create_rollback_script(&self, backup_path: &Path, target_path: &Path) -> Result { use std::os::unix::fs::PermissionsExt; // Use /var/run or agent's own directory instead of /tmp let script_dir = if Path::new("/var/run/gururmm").exists() { PathBuf::from("/var/run/gururmm") } else { // Fall back to agent's config directory PathBuf::from("/etc/gururmm") }; // Ensure directory exists with proper permissions tokio::fs::create_dir_all(&script_dir).await?; let script_path = script_dir.join(format!("rollback-{}.sh", uuid::Uuid::new_v4())); let script = format!(r#"#!/bin/bash set -e # Rollback script for GuruRMM agent update BACKUP="{}" TARGET="{}" SCRIPT_PATH="$0" # Function to cleanup cleanup() {{ rm -f "$SCRIPT_PATH" }} trap cleanup EXIT # Wait for parent process to exit sleep 2 # Restore backup if [ -f "$BACKUP" ]; then cp "$BACKUP" "$TARGET" chmod +x "$TARGET" # Restart service if command -v systemctl &> /dev/null; then systemctl restart gururmm-agent fi fi "#, backup_path.display(), target_path.display() ); // Write script with restrictive permissions tokio::fs::write(&script_path, &script).await?; // Set permissions to 700 (owner only) let perms = std::fs::Permissions::from_mode(0o700); std::fs::set_permissions(&script_path, perms)?; // Verify ownership (should be root) let metadata = std::fs::metadata(&script_path)?; if metadata.uid() != 0 { warn!("Rollback script not owned by root - this may be a security issue"); } Ok(script_path) } ``` --- ### 1.7 Dashboard: Improve Token Storage Security **File:** `dashboard/src/api/client.ts` **Severity:** CRITICAL **Option A: Use httpOnly Cookies (Recommended)** This requires server-side changes. The server should set the token as an httpOnly cookie. **Server-side change (server/src/api/auth.rs):** ```rust use axum::response::{IntoResponse, Response}; use http::header::{SET_COOKIE, HeaderValue}; pub async fn login( State(state): State, Json(req): Json, ) -> Result { // ... existing validation ... let token = create_jwt(&user)?; // Set httpOnly cookie instead of returning token in body let cookie = format!( "auth_token={}; HttpOnly; Secure; SameSite=Strict; Path=/; Max-Age=86400", token ); let mut response = Json(LoginResponse { user_id: user.id, email: user.email, name: user.name, role: user.role, }).into_response(); response.headers_mut().insert( SET_COOKIE, HeaderValue::from_str(&cookie).unwrap() ); Ok(response) } ``` **Dashboard change (client.ts):** ```typescript // dashboard/src/api/client.ts const api = axios.create({ baseURL: import.meta.env.VITE_API_URL || 'https://rmm-api.azcomputerguru.com', withCredentials: true, // Send cookies with requests }); // Remove localStorage token handling - cookies are automatic export const authApi = { login: async (email: string, password: string) => { const response = await api.post('/api/auth/login', { email, password }); // No need to store token - it's in httpOnly cookie return response.data; }, logout: async () => { await api.post('/api/auth/logout'); // Server clears cookie }, // ... }; ``` **Option B: If httpOnly cookies aren't feasible (short-term)** Add XSS protection and token encryption: ```typescript // dashboard/src/api/client.ts import CryptoJS from 'crypto-js'; // Use session-specific key (generated on page load) const SESSION_KEY = CryptoJS.lib.WordArray.random(32).toString(); const encryptToken = (token: string): string => { return CryptoJS.AES.encrypt(token, SESSION_KEY).toString(); }; const decryptToken = (encrypted: string): string | null => { try { const bytes = CryptoJS.AES.decrypt(encrypted, SESSION_KEY); return bytes.toString(CryptoJS.enc.Utf8); } catch { return null; } }; export const authApi = { login: async (email: string, password: string) => { const response = await api.post('/api/auth/login', { email, password }); if (response.data.token) { // Store encrypted token in sessionStorage (cleared on tab close) sessionStorage.setItem('auth_token', encryptToken(response.data.token)); } return response.data; }, getToken: (): string | null => { const encrypted = sessionStorage.getItem('auth_token'); return encrypted ? decryptToken(encrypted) : null; }, // ... }; // Update interceptor api.interceptors.request.use((config) => { const token = authApi.getToken(); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }); ``` --- ### 1.8 Dashboard: Add Command Input Validation **File:** `dashboard/src/pages/AgentDetail.tsx` **Lines:** 236-240 **Severity:** CRITICAL **Add validation and confirmation:** ```typescript // dashboard/src/pages/AgentDetail.tsx import { useState, useCallback } from 'react'; // Add dangerous command patterns const DANGEROUS_PATTERNS = [ /rm\s+-rf\s+\//i, /format\s+[a-z]:/i, /del\s+\/[fqs]/i, /shutdown/i, /reboot/i, /init\s+0/i, /mkfs/i, /dd\s+if=/i, />\s*\/dev\//i, /chmod\s+-R\s+777/i, ]; const WARN_PATTERNS = [ /password/i, /credential/i, /secret/i, /api.?key/i, /token/i, ]; interface CommandValidation { isValid: boolean; isDangerous: boolean; hasWarnings: boolean; messages: string[]; } const validateCommand = (command: string): CommandValidation => { const messages: string[] = []; let isDangerous = false; let hasWarnings = false; // Check for empty command if (!command.trim()) { return { isValid: false, isDangerous: false, hasWarnings: false, messages: ['Command cannot be empty'] }; } // Check for dangerous patterns for (const pattern of DANGEROUS_PATTERNS) { if (pattern.test(command)) { isDangerous = true; messages.push(`[DANGER] Potentially destructive command detected: ${pattern.source}`); } } // Check for warning patterns for (const pattern of WARN_PATTERNS) { if (pattern.test(command)) { hasWarnings = true; messages.push(`[WARNING] Command may expose sensitive data: ${pattern.source}`); } } // Check command length if (command.length > 10000) { messages.push('[WARNING] Command is very long - consider using a script file'); hasWarnings = true; } return { isValid: true, isDangerous, hasWarnings, messages, }; }; // In the component: const [showConfirmDialog, setShowConfirmDialog] = useState(false); const [pendingCommand, setPendingCommand] = useState(''); const [validationResult, setValidationResult] = useState(null); const handleSendCommand = useCallback(async () => { if (!command.trim() || !agent) return; const validation = validateCommand(command); setValidationResult(validation); if (!validation.isValid) { return; } if (validation.isDangerous || validation.hasWarnings) { setPendingCommand(command); setShowConfirmDialog(true); return; } await executeCommand(command); }, [command, agent]); const executeCommand = async (cmd: string) => { try { await agentsApi.sendCommand(agent!.id, { command_type: commandType, command: cmd, timeout_seconds: 300, elevated: false, }); setCommand(''); setShowConfirmDialog(false); setPendingCommand(''); } catch (err) { console.error('Failed to send command:', err); } }; // Add confirmation dialog JSX: {showConfirmDialog && (

{validationResult?.isDangerous ? '[DANGER] Confirm Dangerous Command' : '[WARNING] Confirm Command'}

{validationResult?.messages.map((msg, i) => (

{msg}

))}
        {pendingCommand}
      

This command will be executed on: {agent?.hostname}

)} ``` --- ### 1.9 Legacy Agent: Document Certificate Validation Bypass **File:** `agent-legacy/GuruRMM-Agent.ps1` **Line:** 103 **Severity:** CRITICAL **Cannot be fully fixed** for legacy systems that lack proper certificate chains. **Mitigation: Add explicit parameter and logging:** ```powershell # GuruRMM-Agent.ps1 param( [string]$ServerUrl = "", [string]$SiteCode = "", [switch]$AllowInsecureTLS, # NEW: Must be explicitly set [switch]$Register, [switch]$Run ) # Near line 103, replace: function Initialize-TLS { # Configure TLS 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, falling back to TLS 1.1" "WARN" try { [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls11 } catch { Write-Log "TLS 1.1 not available - connection may be insecure" "ERROR" } } # Certificate validation - ONLY disable if explicitly requested if ($script:AllowInsecureTLS) { Write-Log "[SECURITY WARNING] Certificate validation DISABLED - vulnerable to MITM attacks" "WARN" Write-Log "[SECURITY WARNING] This should only be used on legacy systems with self-signed certificates" "WARN" # Log to Windows Event Log for audit trail try { Write-EventLog -LogName Application -Source "GuruRMM" -EventId 1001 -EntryType Warning ` -Message "GuruRMM agent started with certificate validation disabled. 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 not set (use default validation) [System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null } } # Update installation documentation: <# .SYNOPSIS GuruRMM Legacy Agent for Windows Server 2008 R2 and older systems. .DESCRIPTION This agent is designed for systems that cannot run the modern Rust-based agent. SECURITY WARNING: The -AllowInsecureTLS switch disables certificate validation, making the connection vulnerable to man-in-the-middle attacks. Only use this option on isolated networks or when using self-signed certificates that cannot be properly installed in the system certificate store. .PARAMETER AllowInsecureTLS [SECURITY RISK] Disables SSL/TLS certificate validation. Required for systems with self-signed certificates or broken certificate chains. This flag must be explicitly provided - it is not enabled by default. .EXAMPLE # Secure installation (recommended) .\GuruRMM-Agent.ps1 -Register -ServerUrl "https://rmm.example.com" -SiteCode "ACME-CORP-1234" # Insecure installation (legacy systems only) .\GuruRMM-Agent.ps1 -Register -ServerUrl "https://rmm.example.com" -SiteCode "ACME-CORP-1234" -AllowInsecureTLS #> ``` --- ## Phase 2: Major Issues (Should Fix) ### 2.1 Server: Add Missing Database Index **File:** `server/migrations/005_add_missing_indexes.sql` (new file) ```sql -- Migration: Add missing indexes for performance -- This migration adds indexes that were identified during code review -- Index for agent API key lookups (authentication) CREATE INDEX IF NOT EXISTS idx_agents_api_key_hash ON agents(api_key_hash); -- Index for site API key lookups CREATE INDEX IF NOT EXISTS idx_sites_api_key_hash ON sites(api_key_hash); -- Index for command status queries CREATE INDEX IF NOT EXISTS idx_commands_status ON commands(status); -- Index for metrics time-range queries CREATE INDEX IF NOT EXISTS idx_metrics_agent_timestamp ON metrics(agent_id, timestamp DESC); -- Index for finding online agents CREATE INDEX IF NOT EXISTS idx_agents_status_last_seen ON agents(status, last_seen DESC); ``` --- ### 2.2 Server: Fix WebSocket Ping/Pong Protocol **File:** `server/src/ws/mod.rs` **Lines:** 379-386 **Current (incorrect):** ```rust Ok(Message::Ping(data)) => { if tx .send(ServerMessage::Ack { message_id: None }) .await .is_err() { break; } } ``` **Fixed:** ```rust Ok(Message::Ping(data)) => { // WebSocket protocol requires Pong response to Ping if ws_sender .send(Message::Pong(data)) .await .is_err() { warn!("Failed to send Pong response"); break; } } ``` --- ### 2.3 Agent: Fix Service Path Construction **File:** `agent/src/service.rs` **Line:** 93 **Current:** ```rust let config_path = PathBuf::from(format!(r"{}\\agent.toml", CONFIG_DIR)); ``` **Fixed:** ```rust let config_path = PathBuf::from(CONFIG_DIR).join("agent.toml"); ``` --- ### 2.4 Agent: Fix Rollback Script Backgrounding **File:** `agent/src/updater/mod.rs` **Lines:** 321-327 **Current (broken):** ```rust tokio::process::Command::new("nohup") .arg("bash") .arg(&script_path) .arg("&") // This doesn't work - & is passed as literal argument .spawn() ``` **Fixed:** ```rust // Use setsid to create new session and properly detach tokio::process::Command::new("setsid") .arg("bash") .arg(&script_path) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) .stderr(std::process::Stdio::null()) .spawn() .context("Failed to spawn rollback script")?; // Alternative using nohup correctly: tokio::process::Command::new("bash") .arg("-c") .arg(format!("nohup {} /dev/null 2>&1 &", script_path.display())) .spawn() .context("Failed to spawn rollback script")?; ``` --- ### 2.5 Agent: Replace todo!() with Proper Error **File:** `agent/src/main.rs` **Lines:** 227-229 **Current:** ```rust #[cfg(target_os = "macos")] { todo!("macOS launchd service installation not yet implemented"); } ``` **Fixed:** ```rust #[cfg(target_os = "macos")] { return Err(anyhow::anyhow!( "macOS launchd service installation is not yet implemented. \ Please run the agent manually or create a launchd plist manually. \ See: https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPSystemStartup/Chapters/CreatingLaunchdJobs.html" )); } ``` --- ### 2.6 Dashboard: Fix Duplicate Filter Options **File:** `dashboard/src/pages/Agents.tsx` **Lines:** 175-176 **Current:** ```tsx ``` **Fixed:** ```tsx ``` **Update filtering logic:** ```typescript const filteredAgents = useMemo(() => { let result = agents || []; if (clientFilter === '__unassigned__') { result = result.filter(a => !a.site_id && !a.client_id); } else if (clientFilter) { result = result.filter(a => a.client_id === clientFilter); } if (searchTerm) { const term = searchTerm.toLowerCase(); result = result.filter(a => a.hostname.toLowerCase().includes(term) || a.os_type?.toLowerCase().includes(term) ); } return result; }, [agents, clientFilter, searchTerm]); ``` --- ### 2.7 Dashboard: Add Error Handlers to Mutations **File:** `dashboard/src/pages/Agents.tsx` **Lines:** 98-114 **Current:** ```typescript const deleteMutation = useMutation({ mutationFn: agentsApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['agents'] }); setDeleteConfirm(null); }, }); ``` **Fixed:** ```typescript const deleteMutation = useMutation({ mutationFn: agentsApi.delete, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['agents'] }); setDeleteConfirm(null); // Optional: Show success toast }, onError: (error: AxiosError<{ error: string }>) => { const message = error.response?.data?.error || error.message || 'Failed to delete agent'; // Show error to user alert(`[ERROR] ${message}`); // Or use a toast notification library }, }); const moveMutation = useMutation({ mutationFn: ({ agentId, siteId }: { agentId: string; siteId: string | null }) => agentsApi.move(agentId, siteId), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['agents'] }); setMoveAgent(null); }, onError: (error: AxiosError<{ error: string }>) => { const message = error.response?.data?.error || error.message || 'Failed to move agent'; alert(`[ERROR] ${message}`); }, }); ``` --- ### 2.8 Dashboard: Replace `any` Types **File:** `dashboard/src/pages/Login.tsx` **Line:** 24 **Current:** ```typescript } catch (err: any) { setError(err.response?.data?.error || 'Login failed'); } ``` **Fixed:** ```typescript import { AxiosError } from 'axios'; interface ApiError { error: string; } // In the component: } catch (err) { if (err instanceof AxiosError) { const apiError = err.response?.data as ApiError | undefined; setError(apiError?.error || err.message || 'Login failed'); } else if (err instanceof Error) { setError(err.message); } else { setError('An unexpected error occurred'); } } ``` **Apply same pattern to Register.tsx line 37.** --- ## Phase 3: Warning Level Issues (Recommended) ### 3.1 Add Request Logging/Audit Trail **File:** `server/src/main.rs` ```rust use tower_http::trace::{TraceLayer, DefaultMakeSpan, DefaultOnResponse}; use tracing::Level; // Add to router let app = Router::new() // ... routes ... .layer( TraceLayer::new_for_http() .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) .on_response(DefaultOnResponse::new().level(Level::INFO)) ); ``` --- ### 3.2 Add Health Check with Database Verification **File:** `server/src/main.rs` **Current:** ```rust async fn health_check() -> &'static str { "OK" } ``` **Fixed:** ```rust use serde::Serialize; #[derive(Serialize)] struct HealthResponse { status: &'static str, database: &'static str, version: &'static str, } async fn health_check(State(state): State) -> Json { let db_status = match sqlx::query("SELECT 1") .fetch_one(&state.db) .await { Ok(_) => "healthy", Err(_) => "unhealthy", }; Json(HealthResponse { status: if db_status == "healthy" { "ok" } else { "degraded" }, database: db_status, version: env!("CARGO_PKG_VERSION"), }) } ``` --- ### 3.3 Add Graceful Shutdown **File:** `server/src/main.rs` ```rust use tokio::signal; async fn shutdown_signal() { let ctrl_c = async { signal::ctrl_c() .await .expect("Failed to install Ctrl+C handler"); }; #[cfg(unix)] let terminate = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("Failed to install signal handler") .recv() .await; }; #[cfg(not(unix))] let terminate = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = terminate => {}, } tracing::info!("Shutdown signal received, starting graceful shutdown"); } // In main(): let listener = tokio::net::TcpListener::bind(&addr).await?; axum::serve(listener, app) .with_graceful_shutdown(shutdown_signal()) .await?; ``` --- ## Implementation Checklist ### Critical (Do First) - [ ] 1.1 Restrict CORS configuration - [ ] 1.2 Add AuthUser to all endpoints - [ ] 1.3 Add rate limiting to registration - [ ] 1.4 Remove Unicode characters from agent output - [ ] 1.5 Add certificate pinning for updates - [ ] 1.6 Fix insecure temp file creation - [ ] 1.7 Improve dashboard token storage - [ ] 1.8 Add command input validation - [ ] 1.9 Document/mitigate legacy agent cert bypass ### Major (Do Second) - [ ] 2.1 Add missing database indexes - [ ] 2.2 Fix WebSocket ping/pong - [ ] 2.3 Fix service path construction - [ ] 2.4 Fix rollback script backgrounding - [ ] 2.5 Replace todo!() with proper errors - [ ] 2.6 Fix duplicate filter options - [ ] 2.7 Add error handlers to mutations - [ ] 2.8 Replace `any` types with proper types ### Warnings (Do Third) - [ ] 3.1 Add request logging/audit trail - [ ] 3.2 Add comprehensive health check - [ ] 3.3 Add graceful shutdown handling - [ ] Add pagination to commands endpoint - [ ] Add input validation to all request types - [ ] Implement token expiration handling in dashboard - [ ] Add dark mode toggle (CSS already exists) - [ ] Add log rotation to legacy agent --- ## Testing Requirements After implementing fixes: 1. **Authentication Tests** - Verify all protected endpoints return 401 without token - Verify all protected endpoints work with valid token - Verify rate limiting on registration endpoints 2. **Security Tests** - Verify CORS blocks unauthorized origins - Verify command validation catches dangerous patterns - Verify update downloads only from allowed domains 3. **Functional Tests** - Verify agent registration still works - Verify command execution still works - Verify metrics collection still works - Verify WebSocket reconnection works 4. **Legacy Agent Tests** - Verify agent works without -AllowInsecureTLS (with valid cert) - Verify agent works with -AllowInsecureTLS (with self-signed) - Verify Windows Event Log entries are created --- ## Estimated Effort | Phase | Items | Estimated Time | |-------|-------|----------------| | Phase 1 (Critical) | 9 items | 8-12 hours | | Phase 2 (Major) | 8 items | 4-6 hours | | Phase 3 (Warnings) | 8+ items | 4-8 hours | | Testing | All | 4-6 hours | | **Total** | | **20-32 hours** | --- **Document Version:** 1.0 **Created:** 2026-01-20 **Author:** Claude Code Review System