Files
Redflag/aggregator-server/internal/api/handlers/subsystems.go

413 lines
12 KiB
Go

package handlers
import (
"fmt"
"log"
"net/http"
"time"
"github.com/Fimeg/RedFlag/aggregator-server/internal/command"
"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
commandFactory *command.Factory
signingService *services.SigningService
securityLogger *logging.SecurityLogger
}
func NewSubsystemHandler(sq *queries.SubsystemQueries, cq *queries.CommandQueries, cf *command.Factory, signingService *services.SigningService, securityLogger *logging.SecurityLogger) *SubsystemHandler {
return &SubsystemHandler{
subsystemQueries: sq,
commandQueries: cq,
commandFactory: cf,
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 {
// Generate ID if not set (prevents zero UUID issues)
if cmd.ID == uuid.Nil {
cmd.ID = uuid.New()
}
// Set timestamps if not set
if cmd.CreatedAt.IsZero() {
cmd.CreatedAt = time.Now()
}
if cmd.UpdatedAt.IsZero() {
cmd.UpdatedAt = time.Now()
}
// 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 using factory with idempotency
commandType := "scan_" + subsystem
idempotencyKey := fmt.Sprintf("%s_%s_%d", agentID.String(), subsystem, time.Now().Unix())
// 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))
command, err := h.commandFactory.CreateWithIdempotency(
agentID,
commandType,
map[string]interface{}{"subsystem": subsystem},
idempotencyKey,
)
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
}
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"})
}