diff --git a/aggregator-server/internal/api/handlers/downloads.go b/aggregator-server/internal/api/handlers/downloads.go index 39694c5..7f1cfcb 100644 --- a/aggregator-server/internal/api/handlers/downloads.go +++ b/aggregator-server/internal/api/handlers/downloads.go @@ -2,6 +2,7 @@ package handlers import ( "fmt" + "log" "net/http" "os" "path/filepath" @@ -9,6 +10,7 @@ import ( "github.com/Fimeg/RedFlag/aggregator-server/internal/config" "github.com/Fimeg/RedFlag/aggregator-server/internal/services" + "github.com/google/uuid" "github.com/gin-gonic/gin" ) @@ -97,8 +99,9 @@ func (h *DownloadHandler) DownloadAgent(c *gin.Context) { agentPath = filepath.Join(h.agentDir, "binaries", platform, filename) } - // Check if file exists - if _, err := os.Stat(agentPath); os.IsNotExist(err) { + // Check if file exists and is not empty + info, err := os.Stat(agentPath) + if err != nil { c.JSON(http.StatusNotFound, gin.H{ "error": "Agent binary not found", "platform": platform, @@ -106,6 +109,14 @@ func (h *DownloadHandler) DownloadAgent(c *gin.Context) { }) return } + if info.Size() == 0 { + c.JSON(http.StatusNotFound, gin.H{ + "error": "Agent binary not found (empty file)", + "platform": platform, + "version": version, + }) + return + } // Handle both GET and HEAD requests if c.Request.Method == "HEAD" { @@ -151,20 +162,221 @@ func (h *DownloadHandler) InstallScript(c *gin.Context) { } serverURL := h.getServerURL(c) - scriptContent := h.generateInstallScript(platform, serverURL) + scriptContent := h.generateInstallScript(c, platform, serverURL) c.Header("Content-Type", "text/plain") c.String(http.StatusOK, scriptContent) } -func (h *DownloadHandler) generateInstallScript(platform, baseURL string) string { +// parseAgentID extracts agent ID from header → path → query with security priority +func parseAgentID(c *gin.Context) string { + // 1. Header → Secure (preferred) + if agentID := c.GetHeader("X-Agent-ID"); agentID != "" { + if _, err := uuid.Parse(agentID); err == nil { + log.Printf("[DEBUG] Parsed agent ID from header: %s", agentID) + return agentID + } + log.Printf("[DEBUG] Invalid UUID in header: %s", agentID) + } + + // 2. Path parameter → Legacy compatible + if agentID := c.Param("agent_id"); agentID != "" { + if _, err := uuid.Parse(agentID); err == nil { + log.Printf("[DEBUG] Parsed agent ID from path: %s", agentID) + return agentID + } + log.Printf("[DEBUG] Invalid UUID in path: %s", agentID) + } + + // 3. Query parameter → Fallback + if agentID := c.Query("agent_id"); agentID != "" { + if _, err := uuid.Parse(agentID); err == nil { + log.Printf("[DEBUG] Parsed agent ID from query: %s", agentID) + return agentID + } + log.Printf("[DEBUG] Invalid UUID in query: %s", agentID) + } + + // Return placeholder for fresh installs + log.Printf("[DEBUG] No valid agent ID found, using placeholder") + return "" +} + +// HandleConfigDownload serves agent configuration templates with updated schema +// The install script injects the agent's actual credentials locally after download +func (h *DownloadHandler) HandleConfigDownload(c *gin.Context) { + agentIDParam := c.Param("agent_id") + + // Validate UUID format + parsedAgentID, err := uuid.Parse(agentIDParam) + if err != nil { + log.Printf("Invalid agent ID format for config download: %s, error: %v", agentIDParam, err) + c.JSON(http.StatusBadRequest, gin.H{ + "error": "Invalid agent ID format", + }) + return + } + + // Log for security monitoring + log.Printf("Config template download requested - agent_id: %s, remote_addr: %s", + parsedAgentID.String(), c.ClientIP()) + + // Get server URL for config + serverURL := h.getServerURL(c) + + // Build config template with schema only (no sensitive credentials) + // Credentials are preserved locally by the install script + configTemplate := map[string]interface{}{ + "version": 5, // Current schema version (v5 as of 0.1.23+) + "agent_version": "0.2.0", + "server_url": serverURL, + + // Placeholder credentials - will be replaced by install script + "agent_id": "00000000-0000-0000-0000-000000000000", + "token": "", + "refresh_token": "", + "registration_token": "", + "machine_id": "", + + // Standard configuration with all subsystems + "check_in_interval": 300, + "rapid_polling_enabled": false, + "rapid_polling_until": "0001-01-01T00:00:00Z", + + "network": map[string]interface{}{ + "timeout": 30000000000, + "retry_count": 3, + "retry_delay": 5000000000, + "max_idle_conn": 10, + }, + + "proxy": map[string]interface{}{ + "enabled": false, + }, + + "tls": map[string]interface{}{ + "enabled": false, + "insecure_skip_verify": false, + }, + + "logging": map[string]interface{}{ + "level": "info", + "max_size": 100, + "max_backups": 3, + "max_age": 28, + }, + + "subsystems": map[string]interface{}{ + "system": map[string]interface{}{ + "enabled": true, + "timeout": 10000000000, + "circuit_breaker": map[string]interface{}{ + "enabled": true, + "failure_threshold": 3, + "failure_window": 600000000000, + "open_duration": 1800000000000, + "half_open_attempts": 2, + }, + }, + "filesystem": map[string]interface{}{ + "enabled": true, + "timeout": 10000000000, + "circuit_breaker": map[string]interface{}{ + "enabled": true, + "failure_threshold": 3, + "failure_window": 600000000000, + "open_duration": 1800000000000, + "half_open_attempts": 2, + }, + }, + "network": map[string]interface{}{ + "enabled": true, + "timeout": 30000000000, + "circuit_breaker": map[string]interface{}{ + "enabled": true, + "failure_threshold": 3, + "failure_window": 600000000000, + "open_duration": 1800000000000, + "half_open_attempts": 2, + }, + }, + "processes": map[string]interface{}{ + "enabled": true, + "timeout": 30000000000, + "circuit_breaker": map[string]interface{}{ + "enabled": true, + "failure_threshold": 3, + "failure_window": 600000000000, + "open_duration": 1800000000000, + "half_open_attempts": 2, + }, + }, + "updates": map[string]interface{}{ + "enabled": true, + "timeout": 30000000000, + "circuit_breaker": map[string]interface{}{ + "enabled": false, + "failure_threshold": 0, + "failure_window": 0, + "open_duration": 0, + "half_open_attempts": 0, + }, + }, + "storage": map[string]interface{}{ + "enabled": true, + "timeout": 10000000000, + "circuit_breaker": map[string]interface{}{ + "enabled": true, + "failure_threshold": 3, + "failure_window": 600000000000, + "open_duration": 1800000000000, + "half_open_attempts": 2, + }, + }, + }, + + "security": map[string]interface{}{ + "ed25519_verification": true, + "nonce_validation": true, + "machine_id_binding": true, + }, + } + + // Return config template as JSON + c.Header("Content-Type", "application/json") + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"redflag-config.json\"")) + c.JSON(http.StatusOK, configTemplate) +} + +func (h *DownloadHandler) generateInstallScript(c *gin.Context, platform, baseURL string) string { + // Parse agent ID with defense-in-depth priority + agentIDParam := parseAgentID(c) + + // Extract registration token from query parameters + registrationToken := c.Query("token") + if registrationToken == "" { + return "# Error: registration token is required\n# Please include token in URL: ?token=YOUR_TOKEN\n" + } + + // Determine architecture based on platform string + var arch string + switch platform { + case "linux": + arch = "amd64" // Default for generic linux downloads + case "windows": + arch = "amd64" // Default for generic windows downloads + default: + arch = "amd64" // Fallback + } + // Use template service to generate install scripts - // For generic downloads, use placeholder values + // Pass actual agent ID for upgrades, fallback placeholder for fresh installs script, err := h.installTemplateService.RenderInstallScriptFromBuild( - "", // Will be generated during install + agentIDParam, // Real agent ID or placeholder platform, // Platform (linux/windows) + arch, // Architecture "latest", // Version - fmt.Sprintf("%s/downloads/%s", baseURL, platform), // Binary URL - fmt.Sprintf("%s/api/v1/config/", baseURL), // Config URL (placeholder) + baseURL, // Server base URL + registrationToken, // Registration token from query param ) if err != nil { return fmt.Sprintf("# Error generating install script: %v", err) diff --git a/aggregator-server/internal/services/install_template_service.go b/aggregator-server/internal/services/install_template_service.go index 3ba537c..403bdd3 100644 --- a/aggregator-server/internal/services/install_template_service.go +++ b/aggregator-server/internal/services/install_template_service.go @@ -4,10 +4,12 @@ import ( "bytes" "embed" "fmt" + "log" "strings" "text/template" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" + "github.com/google/uuid" ) //go:embed templates/install/scripts/*.tmpl @@ -25,17 +27,19 @@ func NewInstallTemplateService() *InstallTemplateService { func (s *InstallTemplateService) RenderInstallScript(agent *models.Agent, binaryURL, configURL string) (string, error) { // Define template data data := struct { - AgentID string - BinaryURL string - ConfigURL string - Platform string - Version string + AgentID string + BinaryURL string + ConfigURL string + Platform string + Architecture string + Version string }{ - AgentID: agent.ID.String(), - BinaryURL: binaryURL, - ConfigURL: configURL, - Platform: agent.OSType, - Version: agent.CurrentVersion, + AgentID: agent.ID.String(), + BinaryURL: binaryURL, + ConfigURL: configURL, + Platform: agent.OSType, + Architecture: agent.OSArchitecture, + Version: agent.CurrentVersion, } // Choose template based on platform @@ -63,24 +67,38 @@ func (s *InstallTemplateService) RenderInstallScript(agent *models.Agent, binary // RenderInstallScriptFromBuild renders script using build response func (s *InstallTemplateService) RenderInstallScriptFromBuild( - agentID string, + agentIDParam string, platform string, + architecture string, version string, - binaryURL string, - configURL string, + serverURL string, + registrationToken string, ) (string, error) { + // Extract or generate agent ID + agentID := s.extractOrGenerateAgentID(agentIDParam) + + // Build correct URLs in Go, not templates + binaryURL := fmt.Sprintf("%s/api/v1/downloads/%s-%s?version=%s", serverURL, platform, architecture, version) + configURL := fmt.Sprintf("%s/api/v1/downloads/config/%s", serverURL, agentID) + data := struct { - AgentID string - BinaryURL string - ConfigURL string - Platform string - Version string + AgentID string + BinaryURL string + ConfigURL string + Platform string + Architecture string + Version string + ServerURL string + RegistrationToken string }{ - AgentID: agentID, - BinaryURL: binaryURL, - ConfigURL: configURL, - Platform: platform, - Version: version, + AgentID: agentID, + BinaryURL: binaryURL, + ConfigURL: configURL, + Platform: platform, + Architecture: architecture, + Version: version, + ServerURL: serverURL, + RegistrationToken: registrationToken, } templateName := "templates/install/scripts/linux.sh.tmpl" @@ -100,3 +118,76 @@ func (s *InstallTemplateService) RenderInstallScriptFromBuild( return buf.String(), nil } + +// BuildAgentConfigWithAgentID builds config for an existing agent (for upgrades) +func (s *InstallTemplateService) BuildAgentConfigWithAgentID( + agentID string, + platform string, + architecture string, + version string, + serverURL string, +) (string, error) { + // Validate agent ID + if _, err := uuid.Parse(agentID); err != nil { + return "", fmt.Errorf("invalid agent ID: %w", err) + } + + // Build correct URLs using existing agent ID + binaryURL := fmt.Sprintf("%s/api/v1/downloads/%s-%s?version=%s", serverURL, platform, architecture, version) + configURL := fmt.Sprintf("%s/api/v1/downloads/config/%s", serverURL, agentID) + + data := struct { + AgentID string + BinaryURL string + ConfigURL string + Platform string + Architecture string + Version string + ServerURL string + }{ + AgentID: agentID, + BinaryURL: binaryURL, + ConfigURL: configURL, + Platform: platform, + Architecture: architecture, + Version: version, + ServerURL: serverURL, + } + + templateName := "templates/install/scripts/linux.sh.tmpl" + if strings.Contains(platform, "windows") { + templateName = "templates/install/scripts/windows.ps1.tmpl" + } + + tmpl, err := template.ParseFS(installScriptTemplates, templateName) + if err != nil { + return "", err + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "", err + } + + return buf.String(), nil +} + +// extractOrGenerateAgentID extracts or generates a valid agent ID +func (s *InstallTemplateService) extractOrGenerateAgentID(param string) string { + log.Printf("[DEBUG] extractOrGenerateAgentID received param: %s", param) + + // If we got a real agent ID (UUID format), validate and use it + if param != "" && param != "" { + // Validate it's a UUID + if _, err := uuid.Parse(param); err == nil { + log.Printf("[DEBUG] Using passed UUID: %s", param) + return param + } + log.Printf("[DEBUG] Invalid UUID format, generating new one") + } + + // Placeholder case - generate new UUID for fresh installation + newID := uuid.New().String() + log.Printf("[DEBUG] Generated new UUID: %s", newID) + return newID +}