package handlers import ( "fmt" "log" "net/http" "strconv" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" "github.com/Fimeg/RedFlag/aggregator-server/internal/services" "github.com/Fimeg/RedFlag/aggregator-server/internal/logging" "github.com/gin-gonic/gin" "github.com/google/uuid" ) type DockerHandler struct { updateQueries *queries.UpdateQueries agentQueries *queries.AgentQueries commandQueries *queries.CommandQueries signingService *services.SigningService securityLogger *logging.SecurityLogger } func NewDockerHandler(uq *queries.UpdateQueries, aq *queries.AgentQueries, cq *queries.CommandQueries, signingService *services.SigningService, securityLogger *logging.SecurityLogger) *DockerHandler { return &DockerHandler{ updateQueries: uq, agentQueries: aq, commandQueries: cq, signingService: signingService, securityLogger: securityLogger, } } // signAndCreateCommand signs a command if signing service is enabled, then stores it in the database func (h *DockerHandler) signAndCreateCommand(cmd *models.AgentCommand) error { // Sign the command before storing if h.signingService != nil && h.signingService.IsEnabled() { signature, err := h.signingService.SignCommand(cmd) if err != nil { return fmt.Errorf("failed to sign command: %w", err) } cmd.Signature = signature // Log successful signing if h.securityLogger != nil { h.securityLogger.LogCommandSigned(cmd) } } else { // Log warning if signing disabled log.Printf("[WARNING] Command signing disabled, storing unsigned command") if h.securityLogger != nil { h.securityLogger.LogPrivateKeyNotConfigured() } } // Store in database err := h.commandQueries.CreateCommand(cmd) if err != nil { return fmt.Errorf("failed to create command: %w", err) } return nil } // GetContainers returns Docker containers and images across all agents func (h *DockerHandler) GetContainers(c *gin.Context) { // Parse query parameters page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) agentID := c.Query("agent") status := c.Query("status") filters := &models.UpdateFilters{ PackageType: "docker_image", Page: page, PageSize: pageSize, Status: status, } // Parse agent_id if provided if agentID != "" { if parsedID, err := uuid.Parse(agentID); err == nil { filters.AgentID = parsedID } } // Get Docker updates (which represent container images) updates, total, err := h.updateQueries.ListUpdatesFromState(filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch Docker containers"}) return } // Get agent information for better display agentMap := make(map[uuid.UUID]models.Agent) for _, update := range updates { if _, exists := agentMap[update.AgentID]; !exists { if agent, err := h.agentQueries.GetAgentByID(update.AgentID); err == nil { agentMap[update.AgentID] = *agent } } } // Transform updates into Docker container format containers := make([]models.DockerContainer, 0, len(updates)) uniqueImages := make(map[string]bool) for _, update := range updates { // Extract container info from update metadata containerName := update.PackageName var ports []models.DockerPort if update.Metadata != nil { if name, exists := update.Metadata["container_name"]; exists { if nameStr, ok := name.(string); ok { containerName = nameStr } } // Extract port information from metadata if portsData, exists := update.Metadata["ports"]; exists { if portsArray, ok := portsData.([]interface{}); ok { for _, portData := range portsArray { if portMap, ok := portData.(map[string]interface{}); ok { port := models.DockerPort{} if cp, ok := portMap["container_port"].(float64); ok { port.ContainerPort = int(cp) } if hp, ok := portMap["host_port"].(float64); ok { hostPort := int(hp) port.HostPort = &hostPort } if proto, ok := portMap["protocol"].(string); ok { port.Protocol = proto } if ip, ok := portMap["host_ip"].(string); ok { port.HostIP = ip } else { port.HostIP = "0.0.0.0" } ports = append(ports, port) } } } } } // Get agent information agentInfo := agentMap[update.AgentID] // Create container representation container := models.DockerContainer{ ID: update.ID.String(), ContainerID: containerName, Image: update.PackageName, Tag: update.AvailableVersion, // Available version becomes the tag AgentID: update.AgentID.String(), AgentName: agentInfo.Hostname, AgentHostname: agentInfo.Hostname, Status: update.Status, State: "", // Could be extracted from metadata if available Ports: ports, CreatedAt: update.LastDiscoveredAt, UpdatedAt: update.LastUpdatedAt, UpdateAvailable: update.Status != "installed", CurrentVersion: update.CurrentVersion, AvailableVersion: update.AvailableVersion, } // Add image to unique set imageKey := update.PackageName + ":" + update.AvailableVersion uniqueImages[imageKey] = true containers = append(containers, container) } response := models.DockerContainerListResponse{ Containers: containers, Images: containers, // Alias for containers to match frontend expectation TotalImages: len(uniqueImages), Total: len(containers), Page: page, PageSize: pageSize, TotalPages: (total + pageSize - 1) / pageSize, } c.JSON(http.StatusOK, response) } // GetAgentContainers returns Docker containers for a specific agent func (h *DockerHandler) GetAgentContainers(c *gin.Context) { agentIDStr := c.Param("agent_id") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } // Parse query parameters page, _ := strconv.Atoi(c.DefaultQuery("page", "1")) pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "50")) status := c.Query("status") filters := &models.UpdateFilters{ AgentID: agentID, PackageType: "docker_image", Page: page, PageSize: pageSize, Status: status, } // Get Docker updates for specific agent updates, total, err := h.updateQueries.ListUpdatesFromState(filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch Docker containers for agent"}) return } // Get agent information agentInfo, err := h.agentQueries.GetAgentByID(agentID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "agent not found"}) return } // Transform updates into Docker container format containers := make([]models.DockerContainer, 0, len(updates)) uniqueImages := make(map[string]bool) for _, update := range updates { // Extract container info from update metadata containerName := update.PackageName var ports []models.DockerPort if update.Metadata != nil { if name, exists := update.Metadata["container_name"]; exists { if nameStr, ok := name.(string); ok { containerName = nameStr } } // Extract port information from metadata if portsData, exists := update.Metadata["ports"]; exists { if portsArray, ok := portsData.([]interface{}); ok { for _, portData := range portsArray { if portMap, ok := portData.(map[string]interface{}); ok { port := models.DockerPort{} if cp, ok := portMap["container_port"].(float64); ok { port.ContainerPort = int(cp) } if hp, ok := portMap["host_port"].(float64); ok { hostPort := int(hp) port.HostPort = &hostPort } if proto, ok := portMap["protocol"].(string); ok { port.Protocol = proto } if ip, ok := portMap["host_ip"].(string); ok { port.HostIP = ip } else { port.HostIP = "0.0.0.0" } ports = append(ports, port) } } } } } container := models.DockerContainer{ ID: update.ID.String(), ContainerID: containerName, Image: update.PackageName, Tag: update.AvailableVersion, AgentID: update.AgentID.String(), AgentName: agentInfo.Hostname, AgentHostname: agentInfo.Hostname, Status: update.Status, State: "", // Could be extracted from metadata if available Ports: ports, CreatedAt: update.LastDiscoveredAt, UpdatedAt: update.LastUpdatedAt, UpdateAvailable: update.Status != "installed", CurrentVersion: update.CurrentVersion, AvailableVersion: update.AvailableVersion, } imageKey := update.PackageName + ":" + update.AvailableVersion uniqueImages[imageKey] = true containers = append(containers, container) } response := models.DockerContainerListResponse{ Containers: containers, Images: containers, // Alias for containers to match frontend expectation TotalImages: len(uniqueImages), Total: len(containers), Page: page, PageSize: pageSize, TotalPages: (total + pageSize - 1) / pageSize, } c.JSON(http.StatusOK, response) } // GetStats returns Docker statistics across all agents func (h *DockerHandler) GetStats(c *gin.Context) { // Get all Docker updates filters := &models.UpdateFilters{ PackageType: "docker_image", Page: 1, PageSize: 10000, // Get all for stats } updates, _, err := h.updateQueries.ListUpdatesFromState(filters) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch Docker stats"}) return } stats := models.DockerStats{ TotalContainers: len(updates), TotalImages: 0, UpdatesAvailable: 0, PendingApproval: 0, CriticalUpdates: 0, } // Calculate stats uniqueImages := make(map[string]bool) agentsWithContainers := make(map[uuid.UUID]bool) for _, update := range updates { // Count unique images imageKey := update.PackageName + ":" + update.AvailableVersion uniqueImages[imageKey] = true // Count agents with containers agentsWithContainers[update.AgentID] = true // Count updates available if update.Status != "installed" { stats.UpdatesAvailable++ } // Count pending approval if update.Status == "pending_approval" { stats.PendingApproval++ } // Count critical updates if update.Severity == "critical" { stats.CriticalUpdates++ } } stats.TotalImages = len(uniqueImages) stats.AgentsWithContainers = len(agentsWithContainers) c.JSON(http.StatusOK, stats) } // ApproveUpdate approves a Docker image update func (h *DockerHandler) ApproveUpdate(c *gin.Context) { containerID := c.Param("container_id") imageID := c.Param("image_id") if containerID == "" || imageID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "container_id and image_id are required"}) return } // Parse the update ID from container_id (they're the same in our implementation) updateID, err := uuid.Parse(containerID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"}) return } // Approve the update if err := h.updateQueries.ApproveUpdate(updateID, "admin"); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to approve Docker update"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Docker update approved", "container_id": containerID, "image_id": imageID, }) } // RejectUpdate rejects a Docker image update func (h *DockerHandler) RejectUpdate(c *gin.Context) { containerID := c.Param("container_id") imageID := c.Param("image_id") if containerID == "" || imageID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "container_id and image_id are required"}) return } // Parse the update ID updateID, err := uuid.Parse(containerID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"}) return } // Get the update details to find the agent ID and package name update, err := h.updateQueries.GetUpdateByID(updateID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "update not found"}) return } // For now, we'll mark as rejected (this would need a proper reject method in queries) if err := h.updateQueries.UpdatePackageStatus(update.AgentID, "docker", update.PackageName, "rejected", nil, nil); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to reject Docker update"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Docker update rejected", "container_id": containerID, "image_id": imageID, }) } // InstallUpdate installs a Docker image update immediately func (h *DockerHandler) InstallUpdate(c *gin.Context) { containerID := c.Param("container_id") imageID := c.Param("image_id") if containerID == "" || imageID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "container_id and image_id are required"}) return } // Parse the update ID updateID, err := uuid.Parse(containerID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid container ID"}) return } // Get the update details to find the agent ID update, err := h.updateQueries.GetUpdateByID(updateID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "update not found"}) return } // Create a command for the agent to install the update // This would trigger the agent to pull the new image command := &models.AgentCommand{ ID: uuid.New(), AgentID: update.AgentID, CommandType: models.CommandTypeScanUpdates, // Reuse scan for Docker updates Params: models.JSONB{ "package_type": "docker", "package_name": update.PackageName, "target_version": update.AvailableVersion, "container_id": containerID, }, Status: models.CommandStatusPending, Source: models.CommandSourceManual, // User-initiated Docker update } if err := h.signAndCreateCommand(command); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create Docker update command"}) return } c.JSON(http.StatusOK, gin.H{ "message": "Docker update command sent", "container_id": containerID, "image_id": imageID, "command_id": command.ID, }) }