Files
Redflag/aggregator-server/internal/models/security_event.go
jpetree331 f97d4845af feat(security): A-1 Ed25519 key rotation + A-2 replay attack fixes
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>
2026-03-28 21:25:47 -04:00

111 lines
4.0 KiB
Go

package models
import (
"crypto/sha256"
"fmt"
"time"
"github.com/google/uuid"
)
// SecurityEvent represents a security-related event that occurred
type SecurityEvent struct {
Timestamp time.Time `json:"timestamp" db:"timestamp"`
Level string `json:"level" db:"level"` // CRITICAL, WARNING, INFO, DEBUG
EventType string `json:"event_type" db:"event_type"`
AgentID uuid.UUID `json:"agent_id,omitempty" db:"agent_id"`
Message string `json:"message" db:"message"`
TraceID string `json:"trace_id,omitempty" db:"trace_id"`
IPAddress string `json:"ip_address,omitempty" db:"ip_address"`
Details map[string]interface{} `json:"details,omitempty" db:"details"` // JSON encoded
Metadata map[string]interface{} `json:"metadata,omitempty" db:"metadata"` // JSON encoded
}
// SecurityEventTypes defines all possible security event types
var SecurityEventTypes = struct {
CmdSigned string
CmdSignatureVerificationFailed string
CmdSignatureVerificationSuccess string
UpdateNonceInvalid string
UpdateSignatureVerificationFailed string
MachineIDMismatch string
AuthJWTValidationFailed string
PrivateKeyNotConfigured string
AgentRegistrationFailed string
UnauthorizedAccessAttempt string
ConfigTamperingDetected string
AnomalousBehavior string
}{
CmdSigned: "CMD_SIGNED",
CmdSignatureVerificationFailed: "CMD_SIGNATURE_VERIFICATION_FAILED",
CmdSignatureVerificationSuccess: "CMD_SIGNATURE_VERIFICATION_SUCCESS",
UpdateNonceInvalid: "UPDATE_NONCE_INVALID",
UpdateSignatureVerificationFailed: "UPDATE_SIGNATURE_VERIFICATION_FAILED",
MachineIDMismatch: "MACHINE_ID_MISMATCH",
AuthJWTValidationFailed: "AUTH_JWT_VALIDATION_FAILED",
PrivateKeyNotConfigured: "PRIVATE_KEY_NOT_CONFIGURED",
AgentRegistrationFailed: "AGENT_REGISTRATION_FAILED",
UnauthorizedAccessAttempt: "UNAUTHORIZED_ACCESS_ATTEMPT",
ConfigTamperingDetected: "CONFIG_TAMPERING_DETECTED",
AnomalousBehavior: "ANOMALOUS_BEHAVIOR",
}
// IsCritical returns true if the event is of critical severity
func (e *SecurityEvent) IsCritical() bool {
return e.Level == "CRITICAL"
}
// IsWarning returns true if the event is a warning
func (e *SecurityEvent) IsWarning() bool {
return e.Level == "WARNING"
}
// ShouldLogToDatabase determines if this event should be stored in the database
func (e *SecurityEvent) ShouldLogToDatabase(logToDatabase bool) bool {
return logToDatabase && (e.IsCritical() || e.IsWarning())
}
// HashIPAddress hashes the IP address for privacy
func (e *SecurityEvent) HashIPAddress() {
if e.IPAddress != "" {
hash := sha256.Sum256([]byte(e.IPAddress))
e.IPAddress = fmt.Sprintf("hashed:%x", hash[:8]) // Store first 8 bytes of hash
}
}
// NewSecurityEvent creates a new security event with current timestamp
func NewSecurityEvent(level, eventType string, agentID uuid.UUID, message string) *SecurityEvent {
return &SecurityEvent{
Timestamp: time.Now().UTC(),
Level: level,
EventType: eventType,
AgentID: agentID,
Message: message,
Details: make(map[string]interface{}),
Metadata: make(map[string]interface{}),
}
}
// WithTrace adds a trace ID to the event
func (e *SecurityEvent) WithTrace(traceID string) *SecurityEvent {
e.TraceID = traceID
return e
}
// WithIPAddress adds an IP address to the event
func (e *SecurityEvent) WithIPAddress(ip string) *SecurityEvent {
e.IPAddress = ip
return e
}
// WithDetail adds a key-value detail to the event
func (e *SecurityEvent) WithDetail(key string, value interface{}) *SecurityEvent {
e.Details[key] = value
return e
}
// WithMetadata adds a key-value metadata to the event
func (e *SecurityEvent) WithMetadata(key string, value interface{}) *SecurityEvent {
e.Metadata[key] = value
return e
}