package handlers import ( "fmt" "log" "net/http" "time" "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 SubsystemHandler struct { subsystemQueries *queries.SubsystemQueries commandQueries *queries.CommandQueries signingService *services.SigningService securityLogger *logging.SecurityLogger } func NewSubsystemHandler(sq *queries.SubsystemQueries, cq *queries.CommandQueries, signingService *services.SigningService, securityLogger *logging.SecurityLogger) *SubsystemHandler { return &SubsystemHandler{ subsystemQueries: sq, commandQueries: cq, signingService: signingService, securityLogger: securityLogger, } } // signAndCreateCommand signs a command if signing service is enabled, then stores it in the database func (h *SubsystemHandler) 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 } // GetSubsystems retrieves all subsystems for an agent // GET /api/v1/agents/:id/subsystems func (h *SubsystemHandler) GetSubsystems(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystems, err := h.subsystemQueries.GetSubsystems(agentID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystems"}) return } c.JSON(http.StatusOK, subsystems) } // GetSubsystem retrieves a specific subsystem for an agent // GET /api/v1/agents/:id/subsystems/:subsystem func (h *SubsystemHandler) GetSubsystem(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } sub, err := h.subsystemQueries.GetSubsystem(agentID, subsystem) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystem"}) return } if sub == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusOK, sub) } // UpdateSubsystem updates subsystem configuration // PATCH /api/v1/agents/:id/subsystems/:subsystem func (h *SubsystemHandler) UpdateSubsystem(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } var config models.SubsystemConfig if err := c.ShouldBindJSON(&config); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate interval if provided if config.IntervalMinutes != nil && (*config.IntervalMinutes < 5 || *config.IntervalMinutes > 1440) { c.JSON(http.StatusBadRequest, gin.H{"error": "Interval must be between 5 and 1440 minutes"}) return } err = h.subsystemQueries.UpdateSubsystem(agentID, subsystem, config) if err != nil { if err.Error() == "subsystem not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update subsystem"}) return } c.JSON(http.StatusOK, gin.H{"message": "Subsystem updated successfully"}) } // EnableSubsystem enables a subsystem // POST /api/v1/agents/:id/subsystems/:subsystem/enable func (h *SubsystemHandler) EnableSubsystem(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } err = h.subsystemQueries.EnableSubsystem(agentID, subsystem) if err != nil { if err.Error() == "subsystem not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to enable subsystem"}) return } c.JSON(http.StatusOK, gin.H{"message": "Subsystem enabled successfully"}) } // DisableSubsystem disables a subsystem // POST /api/v1/agents/:id/subsystems/:subsystem/disable func (h *SubsystemHandler) DisableSubsystem(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } err = h.subsystemQueries.DisableSubsystem(agentID, subsystem) if err != nil { if err.Error() == "subsystem not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to disable subsystem"}) return } c.JSON(http.StatusOK, gin.H{"message": "Subsystem disabled successfully"}) } // TriggerSubsystem manually triggers a subsystem scan // POST /api/v1/agents/:id/subsystems/:subsystem/trigger func (h *SubsystemHandler) TriggerSubsystem(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } // Verify subsystem exists and is enabled sub, err := h.subsystemQueries.GetSubsystem(agentID, subsystem) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystem"}) return } if sub == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } if !sub.Enabled { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem is disabled"}) return } // Create command for the subsystem commandType := "scan_" + subsystem command := &models.AgentCommand{ AgentID: agentID, CommandType: commandType, Status: "pending", Source: "manual", // Manual trigger from UI (must be 'manual' or 'system' per DB constraint) } // Log command creation attempt log.Printf("[INFO] [server] [command] creating_scan_command agent_id=%s subsystem=%s command_type=%s timestamp=%s", agentID, subsystem, commandType, time.Now().Format(time.RFC3339)) log.Printf("[HISTORY] [server] [scan_%s] command_creation_started agent_id=%s timestamp=%s", subsystem, agentID, time.Now().Format(time.RFC3339)) err = h.signAndCreateCommand(command) if err != nil { log.Printf("[ERROR] [server] [scan_%s] command_creation_failed agent_id=%s error=%v", subsystem, agentID, err) log.Printf("[HISTORY] [server] [scan_%s] command_creation_failed error=\"%v\" timestamp=%s", subsystem, err, time.Now().Format(time.RFC3339)) c.JSON(http.StatusInternalServerError, gin.H{ "error": fmt.Sprintf("Failed to create %s scan command: %v", subsystem, err), }) return } log.Printf("[SUCCESS] [server] [scan_%s] command_created agent_id=%s command_id=%s timestamp=%s", subsystem, agentID, command.ID, time.Now().Format(time.RFC3339)) log.Printf("[HISTORY] [server] [scan_%s] command_created agent_id=%s command_id=%s timestamp=%s", subsystem, agentID, command.ID, time.Now().Format(time.RFC3339)) c.JSON(http.StatusOK, gin.H{ "message": "Subsystem scan triggered successfully", "command_id": command.ID, }) } // GetSubsystemStats retrieves statistics for a subsystem // GET /api/v1/agents/:id/subsystems/:subsystem/stats func (h *SubsystemHandler) GetSubsystemStats(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } stats, err := h.subsystemQueries.GetSubsystemStats(agentID, subsystem) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to retrieve subsystem stats"}) return } if stats == nil { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusOK, stats) } // SetAutoRun enables or disables auto-run for a subsystem // POST /api/v1/agents/:id/subsystems/:subsystem/auto-run func (h *SubsystemHandler) SetAutoRun(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } var req struct { AutoRun bool `json:"auto_run"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } err = h.subsystemQueries.SetAutoRun(agentID, subsystem, req.AutoRun) if err != nil { if err.Error() == "subsystem not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update auto-run"}) return } c.JSON(http.StatusOK, gin.H{"message": "Auto-run updated successfully"}) } // SetInterval sets the interval for a subsystem // POST /api/v1/agents/:id/subsystems/:subsystem/interval func (h *SubsystemHandler) SetInterval(c *gin.Context) { agentID, err := uuid.Parse(c.Param("id")) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid agent ID"}) return } subsystem := c.Param("subsystem") if subsystem == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "Subsystem name required"}) return } var req struct { IntervalMinutes int `json:"interval_minutes"` } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } // Validate interval if req.IntervalMinutes < 5 || req.IntervalMinutes > 1440 { c.JSON(http.StatusBadRequest, gin.H{"error": "Interval must be between 5 and 1440 minutes"}) return } err = h.subsystemQueries.SetInterval(agentID, subsystem, req.IntervalMinutes) if err != nil { if err.Error() == "subsystem not found" { c.JSON(http.StatusNotFound, gin.H{"error": "Subsystem not found"}) return } c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update interval"}) return } c.JSON(http.StatusOK, gin.H{"message": "Interval updated successfully"}) }