package handlers import ( "fmt" "net/http" "strconv" "strings" "time" "github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries" "github.com/Fimeg/RedFlag/aggregator-server/internal/models" "github.com/gin-gonic/gin" "github.com/google/uuid" ) // DockerReportsHandler handles Docker image reports from agents type DockerReportsHandler struct { dockerQueries *queries.DockerQueries agentQueries *queries.AgentQueries commandQueries *queries.CommandQueries } func NewDockerReportsHandler(dq *queries.DockerQueries, aq *queries.AgentQueries, cq *queries.CommandQueries) *DockerReportsHandler { return &DockerReportsHandler{ dockerQueries: dq, agentQueries: aq, commandQueries: cq, } } // ReportDockerImages handles Docker image reports from agents using event sourcing func (h *DockerReportsHandler) ReportDockerImages(c *gin.Context) { agentID := c.MustGet("agent_id").(uuid.UUID) // Update last_seen timestamp if err := h.agentQueries.UpdateAgentLastSeen(agentID); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update last seen"}) return } var req models.DockerReportRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate command exists and belongs to agent commandID, err := uuid.Parse(req.CommandID) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid command ID format"}) return } command, err := h.commandQueries.GetCommandByID(commandID) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "command not found"}) return } if command.AgentID != agentID { c.JSON(http.StatusForbidden, gin.H{"error": "unauthorized command"}) return } // Convert Docker images to events events := make([]models.StoredDockerImage, 0, len(req.Images)) for _, item := range req.Images { event := models.StoredDockerImage{ ID: uuid.New(), AgentID: agentID, PackageType: "docker_image", PackageName: item.ImageName + ":" + item.ImageTag, CurrentVersion: item.ImageID, AvailableVersion: item.LatestImageID, Severity: item.Severity, RepositorySource: item.RepositorySource, Metadata: convertToJSONB(item.Metadata), EventType: "discovered", CreatedAt: req.Timestamp, } events = append(events, event) } // Store events in batch with error isolation if err := h.dockerQueries.CreateDockerEventsBatch(events); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to record docker image events"}) return } // Update command status to completed result := models.JSONB{ "docker_images_count": len(req.Images), "logged_at": time.Now(), } if err := h.commandQueries.MarkCommandCompleted(commandID, result); err != nil { fmt.Printf("Warning: Failed to mark docker command %s as completed: %v\n", commandID, err) } c.JSON(http.StatusOK, gin.H{ "message": "docker image events recorded", "count": len(events), "command_id": req.CommandID, }) } // GetAgentDockerImages retrieves Docker image updates for a specific agent func (h *DockerReportsHandler) GetAgentDockerImages(c *gin.Context) { agentIDStr := c.Param("agentId") 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")) if page < 1 { page = 1 } if pageSize < 1 || pageSize > 100 { pageSize = 50 } offset := (page - 1) * pageSize imageName := c.Query("image_name") registry := c.Query("registry") severity := c.Query("severity") hasUpdatesStr := c.Query("has_updates") // Build filter filter := &models.DockerFilter{ AgentID: &agentID, ImageName: nil, Registry: nil, Severity: nil, HasUpdates: nil, Limit: &pageSize, Offset: &(offset), } if imageName != "" { filter.ImageName = &imageName } if registry != "" { filter.Registry = ®istry } if severity != "" { filter.Severity = &severity } if hasUpdatesStr != "" { hasUpdates := hasUpdatesStr == "true" filter.HasUpdates = &hasUpdates } // Fetch Docker images result, err := h.dockerQueries.GetDockerImages(filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch docker images"}) return } c.JSON(http.StatusOK, result) } // GetAgentDockerInfo retrieves detailed Docker information for an agent func (h *DockerReportsHandler) GetAgentDockerInfo(c *gin.Context) { agentIDStr := c.Param("agentId") agentID, err := uuid.Parse(agentIDStr) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"}) return } // Get all Docker images for this agent pageSize := 100 offset := 0 filter := &models.DockerFilter{ AgentID: &agentID, Limit: &pageSize, Offset: &offset, } result, err := h.dockerQueries.GetDockerImages(filter) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch docker images"}) return } // Convert to detailed format dockerInfo := make([]models.DockerImageInfo, 0, len(result.Images)) for _, image := range result.Images { info := models.DockerImageInfo{ ID: image.ID.String(), AgentID: image.AgentID.String(), ImageName: extractName(image.PackageName), ImageTag: extractTag(image.PackageName), ImageID: image.CurrentVersion, RepositorySource: image.RepositorySource, SizeBytes: parseImageSize(image.Metadata), CreatedAt: image.CreatedAt.Format(time.RFC3339), HasUpdate: image.AvailableVersion != image.CurrentVersion, LatestImageID: image.AvailableVersion, Severity: image.Severity, Labels: extractLabels(image.Metadata), Metadata: convertInterfaceMapToJSONB(image.Metadata), PackageType: image.PackageType, CurrentVersion: image.CurrentVersion, AvailableVersion: image.AvailableVersion, EventType: image.EventType, CreatedAtTime: image.CreatedAt, } dockerInfo = append(dockerInfo, info) } c.JSON(http.StatusOK, gin.H{ "docker_images": dockerInfo, "is_live": isDockerRecentlyUpdated(result.Images), "total": len(dockerInfo), "updates_available": countUpdates(dockerInfo), }) } // Helper function to extract name from image name func extractName(imageName string) string { // Simple implementation - split by ":" and return everything except last part parts := strings.Split(imageName, ":") if len(parts) > 1 { return strings.Join(parts[:len(parts)-1], ":") } return imageName } // Helper function to extract tag from image name func extractTag(imageName string) string { // Simple implementation - split by ":" and return last part parts := strings.Split(imageName, ":") if len(parts) > 1 { return parts[len(parts)-1] } return "latest" } // Helper function to parse image size from metadata func parseImageSize(metadata models.JSONB) int64 { // Check if size is stored in metadata if sizeStr, ok := metadata["size"].(string); ok { if size, err := strconv.ParseInt(sizeStr, 10, 64); err == nil { return size } } return 0 } // Helper function to extract labels from metadata func extractLabels(metadata models.JSONB) map[string]string { labels := make(map[string]string) if labelsData, ok := metadata["labels"].(map[string]interface{}); ok { for k, v := range labelsData { if str, ok := v.(string); ok { labels[k] = str } } } return labels } // Helper function to check if Docker images are recently updated func isDockerRecentlyUpdated(images []models.StoredDockerImage) bool { if len(images) == 0 { return false } // Check if any image was updated in the last 5 minutes now := time.Now() for _, image := range images { if now.Sub(image.CreatedAt) < 5*time.Minute { return true } } return false } // Helper function to count available updates func countUpdates(images []models.DockerImageInfo) int { count := 0 for _, image := range images { if image.HasUpdate { count++ } } return count } // Helper function to convert map[string]interface{} to models.JSONB func convertToJSONB(data map[string]interface{}) models.JSONB { result := make(map[string]interface{}) for k, v := range data { result[k] = v } return models.JSONB(result) } // Helper function to convert map[string]interface{} to models.JSONB func convertInterfaceMapToJSONB(data models.JSONB) models.JSONB { result := make(map[string]interface{}) for k, v := range data { result[k] = v } return models.JSONB(result) }