WIP: Save current state - security subsystems, migrations, logging

This commit is contained in:
Fimeg
2025-12-16 14:19:59 -05:00
parent f792ab23c7
commit f7c8d23c5d
89 changed files with 8884 additions and 1394 deletions

View File

@@ -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))
}
}

View 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),
})
}

View File

@@ -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,
})
}
}

View File

@@ -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)
}

View File

@@ -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 ""
}

View File

@@ -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,
})
}

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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)
}
}

View 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
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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
}