398 lines
12 KiB
Go
398 lines
12 KiB
Go
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 {
|
|
// 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
|
|
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"})
|
|
}
|