WIP: Save current state - security subsystems, migrations, logging
This commit is contained in:
@@ -5,21 +5,34 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AgentBuildHandler handles agent build operations
|
||||
type AgentBuildHandler struct {
|
||||
agentQueries *queries.AgentQueries
|
||||
}
|
||||
|
||||
// NewAgentBuildHandler creates a new agent build handler
|
||||
func NewAgentBuildHandler(agentQueries *queries.AgentQueries) *AgentBuildHandler {
|
||||
return &AgentBuildHandler{
|
||||
agentQueries: agentQueries,
|
||||
}
|
||||
}
|
||||
|
||||
// BuildAgent handles the agent build endpoint
|
||||
// Deprecated: Use AgentHandler.Rebuild instead
|
||||
func BuildAgent(c *gin.Context) {
|
||||
func (h *AgentBuildHandler) BuildAgent(c *gin.Context) {
|
||||
var req services.AgentSetupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create config builder
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL)
|
||||
// Create config builder with database access
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL, h.agentQueries.DB)
|
||||
|
||||
// Build agent configuration
|
||||
config, err := configBuilder.BuildAgentConfig(req)
|
||||
@@ -62,7 +75,7 @@ func BuildAgent(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetBuildInstructions returns build instructions for manual setup
|
||||
func GetBuildInstructions(c *gin.Context) {
|
||||
func (h *AgentBuildHandler) GetBuildInstructions(c *gin.Context) {
|
||||
agentID := c.Param("agentID")
|
||||
if agentID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "agent ID is required"})
|
||||
@@ -70,7 +83,7 @@ func GetBuildInstructions(c *gin.Context) {
|
||||
}
|
||||
|
||||
instructions := gin.H{
|
||||
"title": "RedFlag Agent Build Instructions",
|
||||
"title": "RedFlag Agent Build Instructions",
|
||||
"agent_id": agentID,
|
||||
"steps": []gin.H{
|
||||
{
|
||||
@@ -139,7 +152,7 @@ func GetBuildInstructions(c *gin.Context) {
|
||||
}
|
||||
|
||||
// DownloadBuildArtifacts provides download links for generated files
|
||||
func DownloadBuildArtifacts(c *gin.Context) {
|
||||
func (h *AgentBuildHandler) DownloadBuildArtifacts(c *gin.Context) {
|
||||
agentID := c.Param("agentID")
|
||||
fileType := c.Param("fileType")
|
||||
buildDir := c.Query("buildDir")
|
||||
@@ -184,4 +197,4 @@ func DownloadBuildArtifacts(c *gin.Context) {
|
||||
|
||||
// Serve file for download
|
||||
c.FileAttachment(filePath, filepath.Base(filePath))
|
||||
}
|
||||
}
|
||||
|
||||
54
aggregator-server/internal/api/handlers/agent_events.go
Normal file
54
aggregator-server/internal/api/handlers/agent_events.go
Normal file
@@ -0,0 +1,54 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type AgentEventsHandler struct {
|
||||
agentQueries *queries.AgentQueries
|
||||
}
|
||||
|
||||
func NewAgentEventsHandler(aq *queries.AgentQueries) *AgentEventsHandler {
|
||||
return &AgentEventsHandler{agentQueries: aq}
|
||||
}
|
||||
|
||||
// GetAgentEvents returns system events for an agent with optional filtering
|
||||
// GET /api/v1/agents/:id/events?severity=error,critical,warning&limit=50
|
||||
func (h *AgentEventsHandler) GetAgentEvents(c *gin.Context) {
|
||||
agentIDStr := c.Param("id")
|
||||
agentID, err := uuid.Parse(agentIDStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid agent ID"})
|
||||
return
|
||||
}
|
||||
|
||||
// Optional query parameters
|
||||
severity := c.Query("severity") // comma-separated filter: error,critical,warning,info
|
||||
limitStr := c.DefaultQuery("limit", "50")
|
||||
limit, err := strconv.Atoi(limitStr)
|
||||
if err != nil || limit < 1 {
|
||||
limit = 50
|
||||
}
|
||||
if limit > 1000 {
|
||||
limit = 1000 // Cap at 1000 to prevent excessive queries
|
||||
}
|
||||
|
||||
// Get events using the agent queries
|
||||
events, err := h.agentQueries.GetAgentEvents(agentID, severity, limit)
|
||||
if err != nil {
|
||||
log.Printf("ERROR: Failed to fetch agent events: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to fetch events"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"events": events,
|
||||
"total": len(events),
|
||||
})
|
||||
}
|
||||
@@ -3,21 +3,33 @@ package handlers
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AgentSetupHandler handles agent setup operations
|
||||
type AgentSetupHandler struct {
|
||||
agentQueries *queries.AgentQueries
|
||||
}
|
||||
|
||||
// NewAgentSetupHandler creates a new agent setup handler
|
||||
func NewAgentSetupHandler(agentQueries *queries.AgentQueries) *AgentSetupHandler {
|
||||
return &AgentSetupHandler{
|
||||
agentQueries: agentQueries,
|
||||
}
|
||||
}
|
||||
|
||||
// SetupAgent handles the agent setup endpoint
|
||||
// Deprecated: Use AgentHandler.Setup instead
|
||||
func SetupAgent(c *gin.Context) {
|
||||
func (h *AgentSetupHandler) SetupAgent(c *gin.Context) {
|
||||
var req services.AgentSetupRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Create config builder
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL)
|
||||
// Create config builder with database access
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL, h.agentQueries.DB)
|
||||
|
||||
// Build agent configuration
|
||||
config, err := configBuilder.BuildAgentConfig(req)
|
||||
@@ -43,14 +55,14 @@ func SetupAgent(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetTemplates returns available agent templates
|
||||
func GetTemplates(c *gin.Context) {
|
||||
configBuilder := services.NewConfigBuilder("")
|
||||
func (h *AgentSetupHandler) GetTemplates(c *gin.Context) {
|
||||
configBuilder := services.NewConfigBuilder("", h.agentQueries.DB)
|
||||
templates := configBuilder.GetTemplates()
|
||||
c.JSON(http.StatusOK, gin.H{"templates": templates})
|
||||
}
|
||||
|
||||
// ValidateConfiguration validates a configuration before deployment
|
||||
func ValidateConfiguration(c *gin.Context) {
|
||||
func (h *AgentSetupHandler) ValidateConfiguration(c *gin.Context) {
|
||||
var config map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&config); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -63,7 +75,7 @@ func ValidateConfiguration(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
configBuilder := services.NewConfigBuilder("")
|
||||
configBuilder := services.NewConfigBuilder("", h.agentQueries.DB)
|
||||
template, exists := configBuilder.GetTemplate(agentType)
|
||||
if !exists {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Unknown agent type"})
|
||||
@@ -77,4 +89,4 @@ func ValidateConfiguration(c *gin.Context) {
|
||||
"agent_type": agentType,
|
||||
"template": template.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -231,7 +231,7 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
// Rollback the updating status
|
||||
h.agentQueries.UpdateAgentUpdatingStatus(req.AgentID, false, nil)
|
||||
log.Printf("Failed to create update command for agent %s: %v", req.AgentID, err)
|
||||
@@ -239,7 +239,28 @@ func (h *AgentUpdateHandler) UpdateAgent(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("✅ Agent update initiated for %s: %s (%s)", agent.Hostname, req.Version, req.Platform)
|
||||
// Log agent update initiation to system_events table
|
||||
event := &models.SystemEvent{
|
||||
ID: uuid.New(),
|
||||
AgentID: &agentIDUUID,
|
||||
EventType: "agent_update",
|
||||
EventSubtype: "initiated",
|
||||
Severity: "info",
|
||||
Component: "agent",
|
||||
Message: fmt.Sprintf("Agent update initiated: %s -> %s (%s)", agent.CurrentVersion, req.Version, req.Platform),
|
||||
Metadata: map[string]interface{}{
|
||||
"old_version": agent.CurrentVersion,
|
||||
"new_version": req.Version,
|
||||
"platform": req.Platform,
|
||||
"source": "web_ui",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := h.agentQueries.CreateSystemEvent(event); err != nil {
|
||||
log.Printf("Warning: Failed to log agent update to system_events: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("[UPDATE] Agent update initiated for %s: %s -> %s (%s)", agent.Hostname, agent.CurrentVersion, req.Version, req.Platform)
|
||||
|
||||
response := models.AgentUpdateResponse{
|
||||
Message: "Update initiated successfully",
|
||||
@@ -345,7 +366,7 @@ func (h *AgentUpdateHandler) BulkUpdateAgents(c *gin.Context) {
|
||||
command.Params["scheduled_at"] = *req.Scheduled
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
// Rollback status
|
||||
h.agentQueries.UpdateAgentUpdatingStatus(agentID, false, nil)
|
||||
errors = append(errors, fmt.Sprintf("Agent %s: failed to create command", agentID))
|
||||
@@ -359,6 +380,27 @@ func (h *AgentUpdateHandler) BulkUpdateAgents(c *gin.Context) {
|
||||
"status": "initiated",
|
||||
})
|
||||
|
||||
// Log each bulk update initiation to system_events table
|
||||
event := &models.SystemEvent{
|
||||
ID: uuid.New(),
|
||||
AgentID: &agentID,
|
||||
EventType: "agent_update",
|
||||
EventSubtype: "initiated",
|
||||
Severity: "info",
|
||||
Component: "agent",
|
||||
Message: fmt.Sprintf("Agent update initiated (bulk): %s -> %s (%s)", agent.CurrentVersion, req.Version, req.Platform),
|
||||
Metadata: map[string]interface{}{
|
||||
"old_version": agent.CurrentVersion,
|
||||
"new_version": req.Version,
|
||||
"platform": req.Platform,
|
||||
"source": "web_ui_bulk",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := h.agentQueries.CreateSystemEvent(event); err != nil {
|
||||
log.Printf("Warning: Failed to log bulk agent update to system_events: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("✅ Bulk update initiated for %s: %s (%s)", agent.Hostname, req.Version, req.Platform)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,10 @@ import (
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/api/middleware"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/logging"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/scheduler"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/utils"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
@@ -20,22 +23,59 @@ type AgentHandler struct {
|
||||
refreshTokenQueries *queries.RefreshTokenQueries
|
||||
registrationTokenQueries *queries.RegistrationTokenQueries
|
||||
subsystemQueries *queries.SubsystemQueries
|
||||
scheduler *scheduler.Scheduler
|
||||
signingService *services.SigningService
|
||||
securityLogger *logging.SecurityLogger
|
||||
checkInInterval int
|
||||
latestAgentVersion string
|
||||
}
|
||||
|
||||
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, rtq *queries.RefreshTokenQueries, regTokenQueries *queries.RegistrationTokenQueries, sq *queries.SubsystemQueries, checkInInterval int, latestAgentVersion string) *AgentHandler {
|
||||
func NewAgentHandler(aq *queries.AgentQueries, cq *queries.CommandQueries, rtq *queries.RefreshTokenQueries, regTokenQueries *queries.RegistrationTokenQueries, sq *queries.SubsystemQueries, scheduler *scheduler.Scheduler, signingService *services.SigningService, securityLogger *logging.SecurityLogger, checkInInterval int, latestAgentVersion string) *AgentHandler {
|
||||
return &AgentHandler{
|
||||
agentQueries: aq,
|
||||
commandQueries: cq,
|
||||
refreshTokenQueries: rtq,
|
||||
registrationTokenQueries: regTokenQueries,
|
||||
subsystemQueries: sq,
|
||||
scheduler: scheduler,
|
||||
signingService: signingService,
|
||||
securityLogger: securityLogger,
|
||||
checkInInterval: checkInInterval,
|
||||
latestAgentVersion: latestAgentVersion,
|
||||
}
|
||||
}
|
||||
|
||||
// signAndCreateCommand signs a command if signing service is enabled, then stores it in the database
|
||||
func (h *AgentHandler) 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
|
||||
}
|
||||
|
||||
// RegisterAgent handles agent registration
|
||||
func (h *AgentHandler) RegisterAgent(c *gin.Context) {
|
||||
var req models.AgentRegistrationRequest
|
||||
@@ -185,6 +225,47 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
log.Printf("DEBUG: Failed to parse metrics JSON: %v", err)
|
||||
}
|
||||
|
||||
// Process buffered events from agent if present
|
||||
if metrics.Metadata != nil {
|
||||
if bufferedEvents, exists := metrics.Metadata["buffered_events"]; exists {
|
||||
if events, ok := bufferedEvents.([]interface{}); ok && len(events) > 0 {
|
||||
stored := 0
|
||||
for _, e := range events {
|
||||
if eventMap, ok := e.(map[string]interface{}); ok {
|
||||
// Extract event fields with type safety
|
||||
eventType := getStringFromMap(eventMap, "event_type")
|
||||
eventSubtype := getStringFromMap(eventMap, "event_subtype")
|
||||
severity := getStringFromMap(eventMap, "severity")
|
||||
component := getStringFromMap(eventMap, "component")
|
||||
message := getStringFromMap(eventMap, "message")
|
||||
|
||||
if eventType != "" && eventSubtype != "" && severity != "" {
|
||||
event := &models.SystemEvent{
|
||||
AgentID: &agentID,
|
||||
EventType: eventType,
|
||||
EventSubtype: eventSubtype,
|
||||
Severity: severity,
|
||||
Component: component,
|
||||
Message: message,
|
||||
Metadata: eventMap["metadata"].(map[string]interface{}),
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.agentQueries.CreateSystemEvent(event); err != nil {
|
||||
log.Printf("Warning: Failed to store buffered event: %v", err)
|
||||
} else {
|
||||
stored++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if stored > 0 {
|
||||
log.Printf("Stored %d buffered events from agent %s", stored, agentID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logging to see what we received
|
||||
log.Printf("DEBUG: Received metrics - Version: '%s', CPU: %.2f, Memory: %.2f",
|
||||
metrics.Version, metrics.CPUPercent, metrics.MemoryPercent)
|
||||
@@ -355,9 +436,10 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
commandItems := make([]models.CommandItem, 0, len(commands))
|
||||
for _, cmd := range commands {
|
||||
commandItems = append(commandItems, models.CommandItem{
|
||||
ID: cmd.ID.String(),
|
||||
Type: cmd.CommandType,
|
||||
Params: cmd.Params,
|
||||
ID: cmd.ID.String(),
|
||||
Type: cmd.CommandType,
|
||||
Params: cmd.Params,
|
||||
Signature: cmd.Signature,
|
||||
})
|
||||
|
||||
// Mark as sent
|
||||
@@ -438,7 +520,7 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
CompletedAt: &now,
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(auditCmd); err != nil {
|
||||
if err := h.signAndCreateCommand(auditCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create audit command for stale heartbeat: %v", err)
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Created audit trail for stale heartbeat cleanup (agent %s)", agentID)
|
||||
@@ -456,6 +538,19 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
// Process command acknowledgments from agent
|
||||
var acknowledgedIDs []string
|
||||
if len(metrics.PendingAcknowledgments) > 0 {
|
||||
// Debug: Check what commands exist for this agent
|
||||
agentCommands, err := h.commandQueries.GetCommandsByAgentID(agentID)
|
||||
if err != nil {
|
||||
log.Printf("DEBUG: Failed to get commands for agent %s: %v", agentID, err)
|
||||
} else {
|
||||
log.Printf("DEBUG: Agent %s has %d total commands in database", agentID, len(agentCommands))
|
||||
for _, cmd := range agentCommands {
|
||||
if cmd.Status == "completed" || cmd.Status == "failed" || cmd.Status == "timed_out" {
|
||||
log.Printf("DEBUG: Completed command found - ID: %s, Status: %s, Type: %s", cmd.ID, cmd.Status, cmd.CommandType)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("DEBUG: Processing %d pending acknowledgments for agent %s: %v", len(metrics.PendingAcknowledgments), agentID, metrics.PendingAcknowledgments)
|
||||
// Verify which commands from agent's pending list have been recorded
|
||||
verified, err := h.commandQueries.VerifyCommandsCompleted(metrics.PendingAcknowledgments)
|
||||
@@ -470,6 +565,19 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// Hybrid Heartbeat: Check for scheduled subsystem jobs during heartbeat mode
|
||||
// This ensures that even in heartbeat mode, scheduled scans can be triggered
|
||||
if h.scheduler != nil {
|
||||
// Only check for scheduled jobs if agent is in heartbeat mode (rapid polling enabled)
|
||||
isHeartbeatMode := rapidPolling != nil && rapidPolling.Enabled
|
||||
if isHeartbeatMode {
|
||||
if err := h.checkAndCreateScheduledCommands(agentID); err != nil {
|
||||
// Log error but don't fail the request - this is enhancement, not core functionality
|
||||
log.Printf("[Heartbeat] Failed to check scheduled commands for agent %s: %v", agentID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
response := models.CommandsResponse{
|
||||
Commands: commandItems,
|
||||
RapidPolling: rapidPolling,
|
||||
@@ -479,6 +587,94 @@ func (h *AgentHandler) GetCommands(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// checkAndCreateScheduledCommands checks if any subsystem jobs are due for the agent
|
||||
// and creates commands for them using the scheduler (following Option A approach)
|
||||
func (h *AgentHandler) checkAndCreateScheduledCommands(agentID uuid.UUID) error {
|
||||
// Get current subsystems for this agent from database
|
||||
subsystems, err := h.subsystemQueries.GetSubsystems(agentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get subsystems: %w", err)
|
||||
}
|
||||
|
||||
// Check each enabled subsystem with auto_run=true
|
||||
now := time.Now()
|
||||
jobsCreated := 0
|
||||
|
||||
for _, subsystem := range subsystems {
|
||||
if !subsystem.Enabled || !subsystem.AutoRun {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check if this subsystem job is due
|
||||
var isDue bool
|
||||
if subsystem.NextRunAt == nil {
|
||||
// No next run time set, it's due
|
||||
isDue = true
|
||||
} else {
|
||||
// Check if next run time has passed
|
||||
isDue = subsystem.NextRunAt.Before(now) || subsystem.NextRunAt.Equal(now)
|
||||
}
|
||||
|
||||
if isDue {
|
||||
// Create the command using scheduler logic (reusing existing safeguards)
|
||||
if err := h.createSubsystemCommand(agentID, subsystem); err != nil {
|
||||
log.Printf("[Heartbeat] Failed to create command for %s subsystem: %v", subsystem.Subsystem, err)
|
||||
continue
|
||||
}
|
||||
jobsCreated++
|
||||
|
||||
// Update next run time in database ONLY after successful command creation
|
||||
if err := h.updateNextRunTime(agentID, subsystem); err != nil {
|
||||
log.Printf("[Heartbeat] Failed to update next run time for %s subsystem: %v", subsystem.Subsystem, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if jobsCreated > 0 {
|
||||
log.Printf("[Heartbeat] Created %d scheduled commands for agent %s", jobsCreated, agentID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createSubsystemCommand creates a subsystem scan command using scheduler's logic
|
||||
func (h *AgentHandler) createSubsystemCommand(agentID uuid.UUID, subsystem models.AgentSubsystem) error {
|
||||
// Check backpressure: skip if agent has too many pending commands
|
||||
pendingCount, err := h.commandQueries.CountPendingCommandsForAgent(agentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to check pending commands: %w", err)
|
||||
}
|
||||
|
||||
// Backpressure threshold (same as scheduler)
|
||||
const backpressureThreshold = 10
|
||||
if pendingCount >= backpressureThreshold {
|
||||
return fmt.Errorf("agent has %d pending commands (threshold: %d), skipping", pendingCount, backpressureThreshold)
|
||||
}
|
||||
|
||||
// Create the command using same format as scheduler
|
||||
cmd := &models.AgentCommand{
|
||||
ID: uuid.New(),
|
||||
AgentID: agentID,
|
||||
CommandType: fmt.Sprintf("scan_%s", subsystem.Subsystem),
|
||||
Params: models.JSONB{},
|
||||
Status: models.CommandStatusPending,
|
||||
Source: models.CommandSourceSystem,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.signAndCreateCommand(cmd); err != nil {
|
||||
return fmt.Errorf("failed to create command: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateNextRunTime updates the last_run_at and next_run_at for a subsystem after creating a command
|
||||
func (h *AgentHandler) updateNextRunTime(agentID uuid.UUID, subsystem models.AgentSubsystem) error {
|
||||
// Use the existing UpdateLastRun method which handles next_run_at calculation
|
||||
return h.subsystemQueries.UpdateLastRun(agentID, subsystem.Subsystem)
|
||||
}
|
||||
|
||||
// ListAgents returns all agents with last scan information
|
||||
func (h *AgentHandler) ListAgents(c *gin.Context) {
|
||||
status := c.Query("status")
|
||||
@@ -546,7 +742,7 @@ func (h *AgentHandler) TriggerScan(c *gin.Context) {
|
||||
Source: models.CommandSourceManual,
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(cmd); err != nil {
|
||||
if err := h.signAndCreateCommand(cmd); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create command"})
|
||||
return
|
||||
}
|
||||
@@ -591,7 +787,7 @@ func (h *AgentHandler) TriggerHeartbeat(c *gin.Context) {
|
||||
Source: models.CommandSourceManual,
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(cmd); err != nil {
|
||||
if err := h.signAndCreateCommand(cmd); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create heartbeat command"})
|
||||
return
|
||||
}
|
||||
@@ -786,7 +982,7 @@ func (h *AgentHandler) TriggerUpdate(c *gin.Context) {
|
||||
Source: models.CommandSourceManual,
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(cmd); err != nil {
|
||||
if err := h.signAndCreateCommand(cmd); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create update command"})
|
||||
return
|
||||
}
|
||||
@@ -827,6 +1023,15 @@ func (h *AgentHandler) RenewToken(c *gin.Context) {
|
||||
log.Printf("Warning: Failed to update last_seen for agent %s: %v", req.AgentID, err)
|
||||
}
|
||||
|
||||
// Update agent version if provided (for upgrade tracking)
|
||||
if req.AgentVersion != "" {
|
||||
if err := h.agentQueries.UpdateAgentVersion(req.AgentID, req.AgentVersion); err != nil {
|
||||
log.Printf("Warning: Failed to update agent version during token renewal for agent %s: %v", req.AgentID, err)
|
||||
} else {
|
||||
log.Printf("Agent %s version updated to %s during token renewal", req.AgentID, req.AgentVersion)
|
||||
}
|
||||
}
|
||||
|
||||
// Update refresh token expiration (sliding window - reset to 90 days from now)
|
||||
// This ensures active agents never need to re-register
|
||||
newExpiry := time.Now().Add(90 * 24 * time.Hour)
|
||||
@@ -1123,7 +1328,7 @@ func (h *AgentHandler) TriggerReboot(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Save command to database
|
||||
if err := h.commandQueries.CreateCommand(cmd); err != nil {
|
||||
if err := h.signAndCreateCommand(cmd); err != nil {
|
||||
log.Printf("Failed to create reboot command: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create reboot command"})
|
||||
return
|
||||
@@ -1179,3 +1384,13 @@ func (h *AgentHandler) GetAgentConfig(c *gin.Context) {
|
||||
"version": time.Now().Unix(), // Simple version timestamp
|
||||
})
|
||||
}
|
||||
|
||||
// getStringFromMap safely extracts a string value from a map
|
||||
func getStringFromMap(m map[string]interface{}, key string) string {
|
||||
if val, exists := m[key]; exists {
|
||||
if str, ok := val.(string); ok {
|
||||
return str
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
@@ -6,23 +6,21 @@ import (
|
||||
"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/golang-jwt/jwt/v5"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
// AuthHandler handles authentication for the web dashboard
|
||||
type AuthHandler struct {
|
||||
jwtSecret string
|
||||
userQueries *queries.UserQueries
|
||||
jwtSecret string
|
||||
adminQueries *queries.AdminQueries
|
||||
}
|
||||
|
||||
// NewAuthHandler creates a new auth handler
|
||||
func NewAuthHandler(jwtSecret string, userQueries *queries.UserQueries) *AuthHandler {
|
||||
func NewAuthHandler(jwtSecret string, adminQueries *queries.AdminQueries) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
jwtSecret: jwtSecret,
|
||||
userQueries: userQueries,
|
||||
jwtSecret: jwtSecret,
|
||||
adminQueries: adminQueries,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,15 +32,15 @@ type LoginRequest struct {
|
||||
|
||||
// LoginResponse represents a login response
|
||||
type LoginResponse struct {
|
||||
Token string `json:"token"`
|
||||
User *models.User `json:"user"`
|
||||
Token string `json:"token"`
|
||||
User *queries.Admin `json:"user"`
|
||||
}
|
||||
|
||||
// UserClaims represents JWT claims for web dashboard users
|
||||
type UserClaims struct {
|
||||
UserID uuid.UUID `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
UserID string `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
@@ -54,8 +52,8 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// Validate credentials against database
|
||||
user, err := h.userQueries.VerifyCredentials(req.Username, req.Password)
|
||||
// Validate credentials against database hash
|
||||
admin, err := h.adminQueries.VerifyAdminCredentials(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"})
|
||||
return
|
||||
@@ -63,9 +61,9 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
|
||||
// Create JWT token for web dashboard
|
||||
claims := UserClaims{
|
||||
UserID: user.ID,
|
||||
Username: user.Username,
|
||||
Role: user.Role,
|
||||
UserID: fmt.Sprintf("%d", admin.ID),
|
||||
Username: admin.Username,
|
||||
Role: "admin", // Always admin for single-admin system
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
|
||||
IssuedAt: jwt.NewNumericDate(time.Now()),
|
||||
@@ -81,7 +79,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, LoginResponse{
|
||||
Token: tokenString,
|
||||
User: user,
|
||||
User: admin,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ func NewAgentBuild(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create config builder
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL)
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL, nil)
|
||||
|
||||
// Build agent configuration
|
||||
config, err := configBuilder.BuildAgentConfig(setupReq)
|
||||
@@ -122,7 +122,7 @@ func UpgradeAgentBuild(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Create config builder
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL)
|
||||
configBuilder := services.NewConfigBuilder(req.ServerURL, nil)
|
||||
|
||||
// Build agent configuration
|
||||
config, err := configBuilder.BuildAgentConfig(setupReq)
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
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"
|
||||
)
|
||||
@@ -14,16 +18,51 @@ 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) *DockerHandler {
|
||||
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
|
||||
@@ -430,7 +469,7 @@ func (h *DockerHandler) InstallUpdate(c *gin.Context) {
|
||||
Source: models.CommandSourceManual, // User-initiated Docker update
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create Docker update command"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/config"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
||||
"github.com/google/uuid"
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -19,13 +20,15 @@ type DownloadHandler struct {
|
||||
agentDir string
|
||||
config *config.Config
|
||||
installTemplateService *services.InstallTemplateService
|
||||
packageQueries *queries.PackageQueries
|
||||
}
|
||||
|
||||
func NewDownloadHandler(agentDir string, cfg *config.Config) *DownloadHandler {
|
||||
func NewDownloadHandler(agentDir string, cfg *config.Config, packageQueries *queries.PackageQueries) *DownloadHandler {
|
||||
return &DownloadHandler{
|
||||
agentDir: agentDir,
|
||||
config: cfg,
|
||||
installTemplateService: services.NewInstallTemplateService(),
|
||||
packageQueries: packageQueries,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -137,13 +140,58 @@ func (h *DownloadHandler) DownloadUpdatePackage(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: Implement actual package serving from database/filesystem
|
||||
// For now, return a placeholder response
|
||||
c.JSON(http.StatusNotImplemented, gin.H{
|
||||
"error": "Update package download not yet implemented",
|
||||
"package_id": packageID,
|
||||
"message": "This will serve the signed update package file",
|
||||
})
|
||||
parsedPackageID, err := uuid.Parse(packageID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid package ID format"})
|
||||
return
|
||||
}
|
||||
|
||||
// Fetch package from database
|
||||
pkg, err := h.packageQueries.GetSignedPackageByID(parsedPackageID)
|
||||
if err != nil {
|
||||
if err.Error() == "update package not found" {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Package not found",
|
||||
"package_id": packageID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("[ERROR] Failed to fetch package %s: %v", packageID, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to retrieve package",
|
||||
"package_id": packageID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Verify file exists on disk
|
||||
if _, err := os.Stat(pkg.BinaryPath); os.IsNotExist(err) {
|
||||
log.Printf("[ERROR] Package file not found on disk: %s", pkg.BinaryPath)
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "Package file not found on disk",
|
||||
"package_id": packageID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Set appropriate headers
|
||||
c.Header("Content-Type", "application/octet-stream")
|
||||
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s\"", filepath.Base(pkg.BinaryPath)))
|
||||
c.Header("X-Package-Version", pkg.Version)
|
||||
c.Header("X-Package-Platform", pkg.Platform)
|
||||
c.Header("X-Package-Architecture", pkg.Architecture)
|
||||
|
||||
if pkg.Signature != "" {
|
||||
c.Header("X-Package-Signature", pkg.Signature)
|
||||
}
|
||||
|
||||
if pkg.Checksum != "" {
|
||||
c.Header("X-Package-Checksum", pkg.Checksum)
|
||||
}
|
||||
|
||||
// Serve the file
|
||||
c.File(pkg.BinaryPath)
|
||||
}
|
||||
|
||||
// InstallScript serves the installation script
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
@@ -48,8 +49,8 @@ func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
|
||||
|
||||
if activeAgents >= h.config.AgentRegistration.MaxSeats {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Maximum agent seats reached",
|
||||
"limit": h.config.AgentRegistration.MaxSeats,
|
||||
"error": "Maximum agent seats reached",
|
||||
"limit": h.config.AgentRegistration.MaxSeats,
|
||||
"current": activeAgents,
|
||||
})
|
||||
return
|
||||
@@ -106,14 +107,19 @@ func (h *RegistrationTokenHandler) GenerateRegistrationToken(c *gin.Context) {
|
||||
if serverURL == "" {
|
||||
serverURL = "localhost:8080" // Fallback for development
|
||||
}
|
||||
installCommand := "curl -sfL https://" + serverURL + "/install | bash -s -- " + token
|
||||
// Use http:// for localhost, correct API endpoint, and query parameter for token
|
||||
protocol := "http://"
|
||||
if serverURL != "localhost:8080" {
|
||||
protocol = "https://"
|
||||
}
|
||||
installCommand := fmt.Sprintf("curl -sfL \"%s%s/api/v1/install/linux?token=%s\" | sudo bash", protocol, serverURL, token)
|
||||
|
||||
response := gin.H{
|
||||
"token": token,
|
||||
"label": request.Label,
|
||||
"expires_at": expiresAt,
|
||||
"token": token,
|
||||
"label": request.Label,
|
||||
"expires_at": expiresAt,
|
||||
"install_command": installCommand,
|
||||
"metadata": metadata,
|
||||
"metadata": metadata,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response)
|
||||
@@ -178,8 +184,8 @@ func (h *RegistrationTokenHandler) ListRegistrationTokens(c *gin.Context) {
|
||||
response := gin.H{
|
||||
"tokens": tokens,
|
||||
"pagination": gin.H{
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"page": page,
|
||||
"limit": limit,
|
||||
"offset": offset,
|
||||
},
|
||||
"stats": stats,
|
||||
@@ -324,14 +330,14 @@ func (h *RegistrationTokenHandler) GetTokenStats(c *gin.Context) {
|
||||
"agent_usage": gin.H{
|
||||
"active_agents": activeAgentCount,
|
||||
"max_seats": h.config.AgentRegistration.MaxSeats,
|
||||
"available": h.config.AgentRegistration.MaxSeats - activeAgentCount,
|
||||
"available": h.config.AgentRegistration.MaxSeats - activeAgentCount,
|
||||
},
|
||||
"security_limits": gin.H{
|
||||
"max_tokens_per_request": h.config.AgentRegistration.MaxTokens,
|
||||
"max_token_duration": "7 days",
|
||||
"token_expiry_default": h.config.AgentRegistration.TokenExpiry,
|
||||
"max_token_duration": "7 days",
|
||||
"token_expiry_default": h.config.AgentRegistration.TokenExpiry,
|
||||
},
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
}
|
||||
|
||||
146
aggregator-server/internal/api/handlers/scanner_config.go
Normal file
146
aggregator-server/internal/api/handlers/scanner_config.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/database/queries"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
// ScannerConfigHandler manages scanner timeout configuration
|
||||
type ScannerConfigHandler struct {
|
||||
queries *queries.ScannerConfigQueries
|
||||
}
|
||||
|
||||
// NewScannerConfigHandler creates a new scanner config handler
|
||||
func NewScannerConfigHandler(db *sqlx.DB) *ScannerConfigHandler {
|
||||
return &ScannerConfigHandler{
|
||||
queries: queries.NewScannerConfigQueries(db),
|
||||
}
|
||||
}
|
||||
|
||||
// GetScannerTimeouts returns current scanner timeout configuration
|
||||
// GET /api/v1/admin/scanner-timeouts
|
||||
// Security: Requires admin authentication (WebAuthMiddleware)
|
||||
func (h *ScannerConfigHandler) GetScannerTimeouts(c *gin.Context) {
|
||||
configs, err := h.queries.GetAllScannerConfigs()
|
||||
if err != nil {
|
||||
log.Printf("[ERROR] Failed to fetch scanner configs: %v", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to fetch scanner configuration",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"scanner_timeouts": configs,
|
||||
"default_timeout_ms": 1800000, // 30 minutes default
|
||||
})
|
||||
}
|
||||
|
||||
// UpdateScannerTimeout updates scanner timeout configuration
|
||||
// PUT /api/v1/admin/scanner-timeouts/:scanner_name
|
||||
// Security: Requires admin authentication + audit logging
|
||||
func (h *ScannerConfigHandler) UpdateScannerTimeout(c *gin.Context) {
|
||||
scannerName := c.Param("scanner_name")
|
||||
if scannerName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "scanner_name is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
TimeoutMs int `json:"timeout_ms" binding:"required,min=1000,max=7200000"` // 1s to 2 hours
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
timeout := time.Duration(req.TimeoutMs) * time.Millisecond
|
||||
|
||||
// Update config
|
||||
if err := h.queries.UpsertScannerConfig(scannerName, timeout); err != nil {
|
||||
log.Printf("[ERROR] Failed to update scanner config for %s: %v", scannerName, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to update scanner configuration",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create audit event in History table (ETHOS compliance)
|
||||
userID := c.MustGet("user_id").(uuid.UUID)
|
||||
/*
|
||||
event := &models.SystemEvent{
|
||||
ID: uuid.New(),
|
||||
EventType: "scanner_config_change",
|
||||
EventSubtype: "timeout_updated",
|
||||
Severity: "info",
|
||||
Component: "admin_api",
|
||||
Message: fmt.Sprintf("Scanner timeout updated: %s = %v", scannerName, timeout),
|
||||
Metadata: map[string]interface{}{
|
||||
"scanner_name": scannerName,
|
||||
"timeout_ms": req.TimeoutMs,
|
||||
"user_id": userID.String(),
|
||||
"source_ip": c.ClientIP(),
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
// TODO: Integrate with event logging system when available
|
||||
*/
|
||||
log.Printf("[AUDIT] User %s updated scanner timeout: %s = %v", userID, scannerName, timeout)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "scanner timeout updated successfully",
|
||||
"scanner_name": scannerName,
|
||||
"timeout_ms": req.TimeoutMs,
|
||||
"timeout_human": timeout.String(),
|
||||
})
|
||||
}
|
||||
|
||||
// ResetScannerTimeout resets scanner timeout to default (30 minutes)
|
||||
// POST /api/v1/admin/scanner-timeouts/:scanner_name/reset
|
||||
// Security: Requires admin authentication + audit logging
|
||||
func (h *ScannerConfigHandler) ResetScannerTimeout(c *gin.Context) {
|
||||
scannerName := c.Param("scanner_name")
|
||||
if scannerName == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{
|
||||
"error": "scanner_name is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
defaultTimeout := 30 * time.Minute
|
||||
|
||||
if err := h.queries.UpsertScannerConfig(scannerName, defaultTimeout); err != nil {
|
||||
log.Printf("[ERROR] Failed to reset scanner config for %s: %v", scannerName, err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "failed to reset scanner configuration",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Audit log
|
||||
userID := c.MustGet("user_id").(uuid.UUID)
|
||||
log.Printf("[AUDIT] User %s reset scanner timeout: %s to default %v", userID, scannerName, defaultTimeout)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "scanner timeout reset to default",
|
||||
"scanner_name": scannerName,
|
||||
"timeout_ms": int(defaultTimeout.Milliseconds()),
|
||||
"timeout_human": defaultTimeout.String(),
|
||||
})
|
||||
}
|
||||
|
||||
// GetScannerConfigQueries provides access to the queries for config_builder.go
|
||||
func (h *ScannerConfigHandler) GetScannerConfigQueries() *queries.ScannerConfigQueries {
|
||||
return h.queries
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/config"
|
||||
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lib/pq"
|
||||
_ "github.com/lib/pq"
|
||||
@@ -64,16 +65,23 @@ func createSharedEnvContentForDisplay(req struct {
|
||||
ServerHost string `json:"serverHost"`
|
||||
ServerPort string `json:"serverPort"`
|
||||
MaxSeats string `json:"maxSeats"`
|
||||
}, jwtSecret string) (string, error) {
|
||||
}, jwtSecret string, signingKeys map[string]string) (string, error) {
|
||||
// Generate .env file content for user to copy
|
||||
envContent := fmt.Sprintf(`# RedFlag Environment Configuration
|
||||
# Generated by web setup - Save this content to ./config/.env
|
||||
# Generated by web setup on 2025-12-13
|
||||
# [WARNING] SECURITY CRITICAL: Backup the signing key or you will lose access to all agents
|
||||
|
||||
# PostgreSQL Configuration (for PostgreSQL container)
|
||||
POSTGRES_DB=%s
|
||||
POSTGRES_USER=%s
|
||||
POSTGRES_PASSWORD=%s
|
||||
|
||||
# RedFlag Security - Ed25519 Signing Keys
|
||||
# These keys are used to cryptographically sign agent updates and commands
|
||||
# BACKUP THE PRIVATE KEY IMMEDIATELY - Store it in a secure location like a password manager
|
||||
REDFLAG_SIGNING_PRIVATE_KEY=%s
|
||||
REDFLAG_SIGNING_PUBLIC_KEY=%s
|
||||
|
||||
# RedFlag Server Configuration
|
||||
REDFLAG_SERVER_HOST=%s
|
||||
REDFLAG_SERVER_PORT=%s
|
||||
@@ -87,8 +95,15 @@ REDFLAG_ADMIN_PASSWORD=%s
|
||||
REDFLAG_JWT_SECRET=%s
|
||||
REDFLAG_TOKEN_EXPIRY=24h
|
||||
REDFLAG_MAX_TOKENS=100
|
||||
REDFLAG_MAX_SEATS=%s`,
|
||||
REDFLAG_MAX_SEATS=%s
|
||||
|
||||
# Security Settings
|
||||
REDFLAG_SECURITY_COMMAND_SIGNING_ENFORCEMENT=strict
|
||||
REDFLAG_SECURITY_NONCE_TIMEOUT=600
|
||||
REDFLAG_SECURITY_LOG_LEVEL=warn
|
||||
`,
|
||||
req.DBName, req.DBUser, req.DBPassword,
|
||||
signingKeys["private_key"], signingKeys["public_key"],
|
||||
req.ServerHost, req.ServerPort,
|
||||
req.DBHost, req.DBPort, req.DBName, req.DBUser, req.DBPassword,
|
||||
req.AdminUser, req.AdminPass, jwtSecret, req.MaxSeats)
|
||||
@@ -136,7 +151,7 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="header">
|
||||
<h1>🚀 RedFlag Server Setup</h1>
|
||||
<h1>[START] RedFlag Server Setup</h1>
|
||||
<p class="subtitle">Configure your RedFlag deployment</p>
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -199,7 +214,7 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn" id="submitBtn">
|
||||
🚀 Configure RedFlag Server
|
||||
[START] Configure RedFlag Server
|
||||
</button>
|
||||
</form>
|
||||
|
||||
@@ -237,12 +252,12 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
|
||||
// Validate inputs
|
||||
if (!formData.adminUser || !formData.adminPassword) {
|
||||
result.innerHTML = '<div class="error">❌ Admin username and password are required</div>';
|
||||
result.innerHTML = '<div class="error">[ERROR] Admin username and password are required</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
if (!formData.dbHost || !formData.dbPort || !formData.dbName || !formData.dbUser || !formData.dbPassword) {
|
||||
result.innerHTML = '<div class="error">❌ All database fields are required</div>';
|
||||
result.innerHTML = '<div class="error">[ERROR] All database fields are required</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -264,10 +279,10 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
|
||||
if (response.ok) {
|
||||
let resultHtml = '<div class="success">';
|
||||
resultHtml += '<h3>✅ Configuration Generated Successfully!</h3>';
|
||||
resultHtml += '<h3>[SUCCESS] Configuration Generated Successfully!</h3>';
|
||||
resultHtml += '<p><strong>Your JWT Secret:</strong> <code style="background: #f3f4f6; padding: 2px 6px; border-radius: 3px;">' + resultData.jwtSecret + '</code> ';
|
||||
resultHtml += '<button onclick="copyJWT(\'' + resultData.jwtSecret + '\')" style="background: #4f46e5; color: white; border: none; padding: 4px 8px; border-radius: 3px; cursor: pointer; font-size: 0.8rem;">📋 Copy</button></p>';
|
||||
resultHtml += '<p><strong>⚠️ Important Next Steps:</strong></p>';
|
||||
resultHtml += '<p><strong>[WARNING] Important Next Steps:</strong></p>';
|
||||
resultHtml += '<div style="background: #fef3c7; border: 1px solid #f59e0b; border-radius: 6px; padding: 15px; margin: 15px 0;">';
|
||||
resultHtml += '<p style="margin: 0; color: #92400e;"><strong>🔧 Complete Setup Required:</strong></p>';
|
||||
resultHtml += '<ol style="margin: 10px 0 0 0; color: #92400e;">';
|
||||
@@ -292,12 +307,12 @@ func (h *SetupHandler) ShowSetupPage(c *gin.Context) {
|
||||
window.envContent = resultData.envContent;
|
||||
|
||||
} else {
|
||||
result.innerHTML = '<div class="error">❌ Error: ' + resultData.error + '</div>';
|
||||
result.innerHTML = '<div class="error">[ERROR] Error: ' + resultData.error + '</div>';
|
||||
submitBtn.disabled = false;
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
} catch (error) {
|
||||
result.innerHTML = '<div class="error">❌ Network error: ' + error.message + '</div>';
|
||||
result.innerHTML = '<div class="error">[ERROR] Network error: ' + error.message + '</div>';
|
||||
submitBtn.disabled = false;
|
||||
loading.style.display = 'none';
|
||||
}
|
||||
@@ -383,6 +398,22 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// SECURITY: Generate Ed25519 signing keypair (critical for v0.2.x)
|
||||
fmt.Println("[START] Generating Ed25519 signing keypair for security...")
|
||||
signingPublicKey, signingPrivateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
fmt.Printf("CRITICAL ERROR: Failed to generate signing keys: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate signing keys. Security features cannot be enabled."})
|
||||
return
|
||||
}
|
||||
|
||||
signingKeys := map[string]string{
|
||||
"public_key": hex.EncodeToString(signingPublicKey),
|
||||
"private_key": hex.EncodeToString(signingPrivateKey),
|
||||
}
|
||||
fmt.Printf("[SUCCESS] Generated Ed25519 keypair - Fingerprint: %s\n", signingKeys["public_key"][:16])
|
||||
fmt.Println("[WARNING] SECURITY WARNING: Backup the private key immediately or you will lose access to all agents!")
|
||||
|
||||
// Step 1: Update PostgreSQL password from bootstrap to user password
|
||||
fmt.Println("Updating PostgreSQL password from bootstrap to user-provided password...")
|
||||
bootstrapPassword := "redflag_bootstrap" // This matches our bootstrap .env
|
||||
@@ -401,7 +432,7 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
|
||||
fmt.Println("Generating configuration content for manual .env file update...")
|
||||
|
||||
// Generate the complete .env file content for the user to copy
|
||||
newEnvContent, err := createSharedEnvContentForDisplay(req, jwtSecret)
|
||||
newEnvContent, err := createSharedEnvContentForDisplay(req, jwtSecret, signingKeys)
|
||||
if err != nil {
|
||||
fmt.Printf("Failed to generate .env content: %v\n", err)
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate configuration content"})
|
||||
@@ -415,6 +446,8 @@ func (h *SetupHandler) ConfigureServer(c *gin.Context) {
|
||||
"manualRestartRequired": true,
|
||||
"manualRestartCommand": "docker-compose down && docker-compose up -d",
|
||||
"configFilePath": "./config/.env",
|
||||
"securityNotice": "[WARNING] A signing key has been generated. BACKUP THE PRIVATE KEY or you will lose access to all agents!",
|
||||
"publicKeyFingerprint": signingKeys["public_key"][:16] + "...",
|
||||
})
|
||||
}
|
||||
|
||||
@@ -458,3 +491,98 @@ func (h *SetupHandler) GenerateSigningKeys(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// ConfigureSecrets creates all Docker secrets automatically
|
||||
func (h *SetupHandler) ConfigureSecrets(c *gin.Context) {
|
||||
// Check if Docker API is available
|
||||
if !services.IsDockerAvailable() {
|
||||
c.JSON(http.StatusServiceUnavailable, gin.H{
|
||||
"error": "Docker API not available",
|
||||
"message": "Docker socket is not mounted. Please ensure the server can access Docker daemon",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Create Docker secrets service
|
||||
dockerSecrets, err := services.NewDockerSecretsService()
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to connect to Docker",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
defer dockerSecrets.Close()
|
||||
|
||||
// Generate all required secrets
|
||||
type SecretConfig struct {
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
secrets := []SecretConfig{
|
||||
{"redflag_admin_password", config.GenerateSecurePassword()},
|
||||
{"redflag_jwt_secret", generateSecureJWTSecret()},
|
||||
{"redflag_db_password", config.GenerateSecurePassword()},
|
||||
}
|
||||
|
||||
// Try to create each secret
|
||||
createdSecrets := []string{}
|
||||
failedSecrets := []string{}
|
||||
|
||||
for _, secret := range secrets {
|
||||
if err := dockerSecrets.CreateSecret(secret.Name, secret.Value); err != nil {
|
||||
failedSecrets = append(failedSecrets, fmt.Sprintf("%s: %v", secret.Name, err))
|
||||
} else {
|
||||
createdSecrets = append(createdSecrets, secret.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate signing keys
|
||||
publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{
|
||||
"error": "Failed to generate signing keys",
|
||||
"details": err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
publicKeyHex := hex.EncodeToString(publicKey)
|
||||
privateKeyHex := hex.EncodeToString(privateKey)
|
||||
|
||||
// Create signing key secret
|
||||
if err := dockerSecrets.CreateSecret("redflag_signing_private_key", privateKeyHex); err != nil {
|
||||
failedSecrets = append(failedSecrets, fmt.Sprintf("redflag_signing_private_key: %v", err))
|
||||
} else {
|
||||
createdSecrets = append(createdSecrets, "redflag_signing_private_key")
|
||||
}
|
||||
|
||||
response := gin.H{
|
||||
"created_secrets": createdSecrets,
|
||||
"public_key": publicKeyHex,
|
||||
"fingerprint": publicKeyHex[:16],
|
||||
}
|
||||
|
||||
if len(failedSecrets) > 0 {
|
||||
response["failed_secrets"] = failedSecrets
|
||||
c.JSON(http.StatusMultiStatus, response)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response)
|
||||
}
|
||||
|
||||
// GenerateSecurePassword generates a secure password for admin/db
|
||||
func generateSecurePassword() string {
|
||||
bytes := make([]byte, 16)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)[:16] // 16 character random password
|
||||
}
|
||||
|
||||
// generateSecureJWTSecret generates a secure JWT secret
|
||||
func generateSecureJWTSecret() string {
|
||||
bytes := make([]byte, 32)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"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"
|
||||
)
|
||||
@@ -12,15 +16,50 @@ import (
|
||||
type SubsystemHandler struct {
|
||||
subsystemQueries *queries.SubsystemQueries
|
||||
commandQueries *queries.CommandQueries
|
||||
signingService *services.SigningService
|
||||
securityLogger *logging.SecurityLogger
|
||||
}
|
||||
|
||||
func NewSubsystemHandler(sq *queries.SubsystemQueries, cq *queries.CommandQueries) *SubsystemHandler {
|
||||
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) {
|
||||
@@ -205,7 +244,7 @@ func (h *SubsystemHandler) TriggerSubsystem(c *gin.Context) {
|
||||
Source: "web_ui", // Manual trigger from UI
|
||||
}
|
||||
|
||||
err = h.commandQueries.CreateCommand(command)
|
||||
err = h.signAndCreateCommand(command)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create command"})
|
||||
return
|
||||
|
||||
@@ -281,6 +281,8 @@ func (h *UnifiedUpdateHandler) ReportLog(c *gin.Context) {
|
||||
"duration_seconds": req.DurationSeconds,
|
||||
"logged_at": time.Now(),
|
||||
}
|
||||
log.Printf("DEBUG: ReportLog - Marking command %s as completed for agent %s", commandID, agentID)
|
||||
|
||||
|
||||
if req.Result == "success" || req.Result == "completed" {
|
||||
if err := h.commandQueries.MarkCommandCompleted(commandID, result); err != nil {
|
||||
@@ -446,12 +448,12 @@ func (h *UnifiedUpdateHandler) InstallUpdate(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(heartbeatCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", update.AgentID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dry run command"})
|
||||
return
|
||||
}
|
||||
@@ -518,12 +520,12 @@ func (h *UnifiedUpdateHandler) ReportDependencies(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(heartbeatCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", agentID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create installation command"})
|
||||
return
|
||||
}
|
||||
@@ -592,12 +594,12 @@ func (h *UnifiedUpdateHandler) ConfirmDependencies(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(heartbeatCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", update.AgentID, err)
|
||||
}
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create confirmation command"})
|
||||
return
|
||||
}
|
||||
@@ -735,8 +737,32 @@ func (h *UnifiedUpdateHandler) RetryCommand(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
newCommand, err := h.commandQueries.RetryCommand(id)
|
||||
// Get the original command
|
||||
original, err := h.commandQueries.GetCommandByID(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to get original command: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
// Only allow retry of failed, timed_out, or cancelled commands
|
||||
if original.Status != "failed" && original.Status != "timed_out" && original.Status != "cancelled" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "command must be failed, timed_out, or cancelled to retry"})
|
||||
return
|
||||
}
|
||||
|
||||
// Create new command with same parameters, linking it to the original
|
||||
newCommand := &models.AgentCommand{
|
||||
ID: uuid.New(),
|
||||
AgentID: original.AgentID,
|
||||
CommandType: original.CommandType,
|
||||
Params: original.Params,
|
||||
Status: models.CommandStatusPending,
|
||||
CreatedAt: time.Now(),
|
||||
RetriedFromID: &id,
|
||||
}
|
||||
|
||||
// Sign and store the new command
|
||||
if err := h.agentHandler.signAndCreateCommand(newCommand); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("failed to retry command: %v", err)})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -484,7 +484,7 @@ func (h *UpdateHandler) InstallUpdate(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(heartbeatCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", update.AgentID, err)
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Command created for agent %s before dry run", update.AgentID)
|
||||
@@ -494,7 +494,7 @@ func (h *UpdateHandler) InstallUpdate(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Store the dry run command in database
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create dry run command"})
|
||||
return
|
||||
}
|
||||
@@ -591,7 +591,7 @@ func (h *UpdateHandler) ReportDependencies(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(heartbeatCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", agentID, err)
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Command created for agent %s before installation", agentID)
|
||||
@@ -600,7 +600,7 @@ func (h *UpdateHandler) ReportDependencies(c *gin.Context) {
|
||||
log.Printf("[Heartbeat] Skipping heartbeat command for agent %s (already active)", agentID)
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create installation command"})
|
||||
return
|
||||
}
|
||||
@@ -673,7 +673,7 @@ func (h *UpdateHandler) ConfirmDependencies(c *gin.Context) {
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := h.commandQueries.CreateCommand(heartbeatCmd); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(heartbeatCmd); err != nil {
|
||||
log.Printf("[Heartbeat] Warning: Failed to create heartbeat command for agent %s: %v", update.AgentID, err)
|
||||
} else {
|
||||
log.Printf("[Heartbeat] Command created for agent %s before confirm dependencies", update.AgentID)
|
||||
@@ -683,7 +683,7 @@ func (h *UpdateHandler) ConfirmDependencies(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Store the command in database
|
||||
if err := h.commandQueries.CreateCommand(command); err != nil {
|
||||
if err := h.agentHandler.signAndCreateCommand(command); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create confirmation command"})
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user