Complete RedFlag codebase with two major security audit implementations.
== A-1: Ed25519 Key Rotation Support ==
Server:
- SignCommand sets SignedAt timestamp and KeyID on every signature
- signing_keys database table (migration 020) for multi-key rotation
- InitializePrimaryKey registers active key at startup
- /api/v1/public-keys endpoint for rotation-aware agents
- SigningKeyQueries for key lifecycle management
Agent:
- Key-ID-aware verification via CheckKeyRotation
- FetchAndCacheAllActiveKeys for rotation pre-caching
- Cache metadata with TTL and staleness fallback
- SecurityLogger events for key rotation and command signing
== A-2: Replay Attack Fixes (F-1 through F-7) ==
F-5 CRITICAL - RetryCommand now signs via signAndCreateCommand
F-1 HIGH - v3 format: "{agent_id}:{cmd_id}:{type}:{hash}:{ts}"
F-7 HIGH - Migration 026: expires_at column with partial index
F-6 HIGH - GetPendingCommands/GetStuckCommands filter by expires_at
F-2 HIGH - Agent-side executedIDs dedup map with cleanup
F-4 HIGH - commandMaxAge reduced from 24h to 4h
F-3 CRITICAL - Old-format commands rejected after 48h via CreatedAt
Verification fixes: migration idempotency (ETHOS #4), log format
compliance (ETHOS #1), stale comments updated.
All 24 tests passing. Docker --no-cache build verified.
See docs/ for full audit reports and deviation log (DEV-001 to DEV-019).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
205 lines
5.8 KiB
Plaintext
205 lines
5.8 KiB
Plaintext
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/services"
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// SecuritySettingsHandler handles security settings API endpoints
|
|
type SecuritySettingsHandler struct {
|
|
securitySettingsService *services.SecuritySettingsService
|
|
}
|
|
|
|
// NewSecuritySettingsHandler creates a new security settings handler
|
|
func NewSecuritySettingsHandler(securitySettingsService *services.SecuritySettingsService) *SecuritySettingsHandler {
|
|
return &SecuritySettingsHandler{
|
|
securitySettingsService: securitySettingsService,
|
|
}
|
|
}
|
|
|
|
// GetAllSecuritySettings returns all security settings for the authenticated user
|
|
func (h *SecuritySettingsHandler) GetAllSecuritySettings(c *gin.Context) {
|
|
// Get user from context
|
|
userID := c.GetString("user_id")
|
|
|
|
settings, err := h.securitySettingsService.GetAllSettings(userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"settings": settings,
|
|
"user_has_permission": true, // Check actual permissions
|
|
})
|
|
}
|
|
|
|
// GetSecuritySettingsByCategory returns settings for a specific category
|
|
func (h *SecuritySettingsHandler) GetSecuritySettingsByCategory(c *gin.Context) {
|
|
category := c.Param("category") // e.g., "command_signing", "nonce_validation"
|
|
userID := c.GetString("user_id")
|
|
|
|
settings, err := h.securitySettingsService.GetSettingsByCategory(userID, category)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, settings)
|
|
}
|
|
|
|
// UpdateSecuritySetting updates a specific security setting
|
|
func (h *SecuritySettingsHandler) UpdateSecuritySetting(c *gin.Context) {
|
|
var req struct {
|
|
Value interface{} `json:"value" binding:"required"`
|
|
Reason string `json:"reason"` // Optional audit trail
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
category := c.Param("category")
|
|
key := c.Param("key")
|
|
userID := c.GetString("user_id")
|
|
|
|
// Validate before applying
|
|
if err := h.securitySettingsService.ValidateSetting(category, key, req.Value); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Apply the setting
|
|
err := h.securitySettingsService.SetSetting(category, key, req.Value, userID, req.Reason)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
// Return updated setting
|
|
setting, err := h.securitySettingsService.GetSetting(category, key)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "Setting updated successfully",
|
|
"setting": map[string]interface{}{
|
|
"category": category,
|
|
"key": key,
|
|
"value": setting,
|
|
},
|
|
})
|
|
}
|
|
|
|
// ValidateSecuritySettings validates settings without applying them
|
|
func (h *SecuritySettingsHandler) ValidateSecuritySettings(c *gin.Context) {
|
|
var req struct {
|
|
Category string `json:"category" binding:"required"`
|
|
Key string `json:"key" binding:"required"`
|
|
Value interface{} `json:"value" binding:"required"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
err := h.securitySettingsService.ValidateSetting(req.Category, req.Key, req.Value)
|
|
if err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"valid": false,
|
|
"error": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"valid": true,
|
|
"message": "Setting is valid",
|
|
})
|
|
}
|
|
|
|
// GetSecurityAuditTrail returns audit trail of security setting changes
|
|
func (h *SecuritySettingsHandler) GetSecurityAuditTrail(c *gin.Context) {
|
|
// Pagination parameters
|
|
page := c.DefaultQuery("page", "1")
|
|
pageSize := c.DefaultQuery("page_size", "50")
|
|
|
|
pageInt, _ := strconv.Atoi(page)
|
|
pageSizeInt, _ := strconv.Atoi(pageSize)
|
|
|
|
auditEntries, totalCount, err := h.securitySettingsService.GetAuditTrail(pageInt, pageSizeInt)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"audit_entries": auditEntries,
|
|
"pagination": gin.H{
|
|
"page": pageInt,
|
|
"page_size": pageSizeInt,
|
|
"total": totalCount,
|
|
"total_pages": (totalCount + pageSizeInt - 1) / pageSizeInt,
|
|
},
|
|
})
|
|
}
|
|
|
|
// GetSecurityOverview returns current security status overview
|
|
func (h *SecuritySettingsHandler) GetSecurityOverview(c *gin.Context) {
|
|
userID := c.GetString("user_id")
|
|
|
|
overview, err := h.securitySettingsService.GetSecurityOverview(userID)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, overview)
|
|
}
|
|
|
|
// ApplySecuritySettings applies batch of setting changes atomically
|
|
func (h *SecuritySettingsHandler) ApplySecuritySettings(c *gin.Context) {
|
|
var req struct {
|
|
Settings map[string]map[string]interface{} `json:"settings" binding:"required"`
|
|
Reason string `json:"reason"`
|
|
}
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
userID := c.GetString("user_id")
|
|
|
|
// Validate all settings first
|
|
for category, settings := range req.Settings {
|
|
for key, value := range settings {
|
|
if err := h.securitySettingsService.ValidateSetting(category, key, value); err != nil {
|
|
c.JSON(http.StatusBadRequest, gin.H{
|
|
"error": fmt.Sprintf("Validation failed for %s.%s: %v", category, key, err),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Apply all settings atomically
|
|
err := h.securitySettingsService.ApplySettingsBatch(req.Settings, userID, req.Reason)
|
|
if err != nil {
|
|
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"message": "All settings applied successfully",
|
|
"applied_count": len(req.Settings),
|
|
})
|
|
} |