364 lines
11 KiB
Go
364 lines
11 KiB
Go
package logging
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Fimeg/RedFlag/aggregator-server/internal/models"
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"gopkg.in/natefinch/lumberjack.v2"
|
|
)
|
|
|
|
// SecurityLogConfig holds configuration for security logging
|
|
type SecurityLogConfig struct {
|
|
Enabled bool `yaml:"enabled" env:"REDFLAG_SECURITY_LOG_ENABLED" default:"true"`
|
|
Level string `yaml:"level" env:"REDFLAG_SECURITY_LOG_LEVEL" default:"warning"` // none, error, warn, info, debug
|
|
LogSuccesses bool `yaml:"log_successes" env:"REDFLAG_SECURITY_LOG_SUCCESSES" default:"false"`
|
|
FilePath string `yaml:"file_path" env:"REDFLAG_SECURITY_LOG_PATH" default:"/var/log/redflag/security.json"`
|
|
MaxSizeMB int `yaml:"max_size_mb" env:"REDFLAG_SECURITY_LOG_MAX_SIZE" default:"100"`
|
|
MaxFiles int `yaml:"max_files" env:"REDFLAG_SECURITY_LOG_MAX_FILES" default:"10"`
|
|
RetentionDays int `yaml:"retention_days" env:"REDFLAG_SECURITY_LOG_RETENTION" default:"90"`
|
|
LogToDatabase bool `yaml:"log_to_database" env:"REDFLAG_SECURITY_LOG_TO_DB" default:"true"`
|
|
HashIPAddresses bool `yaml:"hash_ip_addresses" env:"REDFLAG_SECURITY_LOG_HASH_IP" default:"true"`
|
|
}
|
|
|
|
// SecurityLogger handles structured security event logging
|
|
type SecurityLogger struct {
|
|
config SecurityLogConfig
|
|
logger *log.Logger
|
|
db *sqlx.DB
|
|
lumberjack *lumberjack.Logger
|
|
mu sync.RWMutex
|
|
buffer chan *models.SecurityEvent
|
|
bufferSize int
|
|
stopChan chan struct{}
|
|
wg sync.WaitGroup
|
|
}
|
|
|
|
// NewSecurityLogger creates a new security logger instance
|
|
func NewSecurityLogger(config SecurityLogConfig, db *sqlx.DB) (*SecurityLogger, error) {
|
|
if !config.Enabled || config.Level == "none" {
|
|
return &SecurityLogger{
|
|
config: config,
|
|
logger: log.New(os.Stdout, "[SECURITY] ", log.LstdFlags|log.LUTC),
|
|
}, nil
|
|
}
|
|
|
|
// Ensure log directory exists
|
|
logDir := filepath.Dir(config.FilePath)
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
return nil, fmt.Errorf("failed to create security log directory: %w", err)
|
|
}
|
|
|
|
// Setup rotating file writer
|
|
lumberjack := &lumberjack.Logger{
|
|
Filename: config.FilePath,
|
|
MaxSize: config.MaxSizeMB,
|
|
MaxBackups: config.MaxFiles,
|
|
MaxAge: config.RetentionDays,
|
|
Compress: true,
|
|
}
|
|
|
|
logger := &SecurityLogger{
|
|
config: config,
|
|
logger: log.New(lumberjack, "", 0), // No prefix, we'll add timestamps ourselves
|
|
db: db,
|
|
lumberjack: lumberjack,
|
|
buffer: make(chan *models.SecurityEvent, 1000),
|
|
bufferSize: 1000,
|
|
stopChan: make(chan struct{}),
|
|
}
|
|
|
|
// Start background processor
|
|
logger.wg.Add(1)
|
|
go logger.processEvents()
|
|
|
|
return logger, nil
|
|
}
|
|
|
|
// Log writes a security event
|
|
func (sl *SecurityLogger) Log(event *models.SecurityEvent) error {
|
|
if !sl.config.Enabled || sl.config.Level == "none" {
|
|
return nil
|
|
}
|
|
|
|
// Skip successes unless configured to log them
|
|
if !sl.config.LogSuccesses && event.EventType == models.SecurityEventTypes.CmdSignatureVerificationSuccess {
|
|
return nil
|
|
}
|
|
|
|
// Filter by log level
|
|
if !sl.shouldLogLevel(event.Level) {
|
|
return nil
|
|
}
|
|
|
|
// Hash IP addresses if configured
|
|
if sl.config.HashIPAddresses && event.IPAddress != "" {
|
|
event.HashIPAddress()
|
|
}
|
|
|
|
// Try to send to buffer (non-blocking)
|
|
select {
|
|
case sl.buffer <- event:
|
|
default:
|
|
// Buffer full, log directly synchronously
|
|
return sl.writeEvent(event)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LogCommandVerificationFailure logs a command signature verification failure
|
|
func (sl *SecurityLogger) LogCommandVerificationFailure(agentID, commandID uuid.UUID, reason string) {
|
|
event := models.NewSecurityEvent("CRITICAL", models.SecurityEventTypes.CmdSignatureVerificationFailed, agentID, "Command signature verification failed")
|
|
event.WithDetail("command_id", commandID.String())
|
|
event.WithDetail("reason", reason)
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogUpdateSignatureValidationFailure logs an update signature validation failure
|
|
func (sl *SecurityLogger) LogUpdateSignatureValidationFailure(agentID uuid.UUID, updateID string, reason string) {
|
|
event := models.NewSecurityEvent("CRITICAL", models.SecurityEventTypes.UpdateSignatureVerificationFailed, agentID, "Update signature validation failed")
|
|
event.WithDetail("update_id", updateID)
|
|
event.WithDetail("reason", reason)
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogCommandSigned logs successful command signing
|
|
func (sl *SecurityLogger) LogCommandSigned(cmd *models.AgentCommand) {
|
|
event := models.NewSecurityEvent("INFO", models.SecurityEventTypes.CmdSigned, cmd.AgentID, "Command signed successfully")
|
|
event.WithDetail("command_id", cmd.ID.String())
|
|
event.WithDetail("command_type", cmd.CommandType)
|
|
event.WithDetail("signature_present", cmd.Signature != "")
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogNonceValidationFailure logs a nonce validation failure
|
|
func (sl *SecurityLogger) LogNonceValidationFailure(agentID uuid.UUID, nonce string, reason string) {
|
|
event := models.NewSecurityEvent("WARNING", models.SecurityEventTypes.UpdateNonceInvalid, agentID, "Update nonce validation failed")
|
|
event.WithDetail("nonce", nonce)
|
|
event.WithDetail("reason", reason)
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogMachineIDMismatch logs a machine ID mismatch
|
|
func (sl *SecurityLogger) LogMachineIDMismatch(agentID uuid.UUID, expected, actual string) {
|
|
event := models.NewSecurityEvent("WARNING", models.SecurityEventTypes.MachineIDMismatch, agentID, "Machine ID mismatch detected")
|
|
event.WithDetail("expected_machine_id", expected)
|
|
event.WithDetail("actual_machine_id", actual)
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogAuthJWTValidationFailure logs a JWT validation failure
|
|
func (sl *SecurityLogger) LogAuthJWTValidationFailure(agentID uuid.UUID, token string, reason string) {
|
|
event := models.NewSecurityEvent("WARNING", models.SecurityEventTypes.AuthJWTValidationFailed, agentID, "JWT authentication failed")
|
|
event.WithDetail("reason", reason)
|
|
if len(token) > 0 {
|
|
event.WithDetail("token_preview", token[:min(len(token), 20)]+"...")
|
|
}
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogPrivateKeyNotConfigured logs when private key is not configured
|
|
func (sl *SecurityLogger) LogPrivateKeyNotConfigured() {
|
|
event := models.NewSecurityEvent("CRITICAL", models.SecurityEventTypes.PrivateKeyNotConfigured, uuid.Nil, "Private signing key not configured")
|
|
event.WithDetail("component", "server")
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogAgentRegistrationFailed logs an agent registration failure
|
|
func (sl *SecurityLogger) LogAgentRegistrationFailed(ip string, reason string) {
|
|
event := models.NewSecurityEvent("WARNING", models.SecurityEventTypes.AgentRegistrationFailed, uuid.Nil, "Agent registration failed")
|
|
event.WithIPAddress(ip)
|
|
event.WithDetail("reason", reason)
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// LogUnauthorizedAccessAttempt logs an unauthorized access attempt
|
|
func (sl *SecurityLogger) LogUnauthorizedAccessAttempt(ip, endpoint, reason string, agentID uuid.UUID) {
|
|
event := models.NewSecurityEvent("WARNING", models.SecurityEventTypes.UnauthorizedAccessAttempt, agentID, "Unauthorized access attempt")
|
|
event.WithIPAddress(ip)
|
|
event.WithDetail("endpoint", endpoint)
|
|
event.WithDetail("reason", reason)
|
|
|
|
_ = sl.Log(event)
|
|
}
|
|
|
|
// processEvents processes events from the buffer in the background
|
|
func (sl *SecurityLogger) processEvents() {
|
|
defer sl.wg.Done()
|
|
|
|
ticker := time.NewTicker(5 * time.Second)
|
|
defer ticker.Stop()
|
|
|
|
batch := make([]*models.SecurityEvent, 0, 100)
|
|
|
|
for {
|
|
select {
|
|
case event := <-sl.buffer:
|
|
batch = append(batch, event)
|
|
if len(batch) >= 100 {
|
|
sl.processBatch(batch)
|
|
batch = batch[:0]
|
|
}
|
|
case <-ticker.C:
|
|
if len(batch) > 0 {
|
|
sl.processBatch(batch)
|
|
batch = batch[:0]
|
|
}
|
|
case <-sl.stopChan:
|
|
// Process any remaining events
|
|
for len(sl.buffer) > 0 {
|
|
batch = append(batch, <-sl.buffer)
|
|
}
|
|
if len(batch) > 0 {
|
|
sl.processBatch(batch)
|
|
}
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// processBatch processes a batch of events
|
|
func (sl *SecurityLogger) processBatch(events []*models.SecurityEvent) {
|
|
for _, event := range events {
|
|
_ = sl.writeEvent(event)
|
|
}
|
|
}
|
|
|
|
// writeEvent writes an event to the configured outputs
|
|
func (sl *SecurityLogger) writeEvent(event *models.SecurityEvent) error {
|
|
// Write to file
|
|
if err := sl.writeToFile(event); err != nil {
|
|
log.Printf("[ERROR] Failed to write security event to file: %v", err)
|
|
}
|
|
|
|
// Write to database if configured
|
|
if sl.config.LogToDatabase && sl.db != nil && event.ShouldLogToDatabase(sl.config.LogToDatabase) {
|
|
if err := sl.writeToDatabase(event); err != nil {
|
|
log.Printf("[ERROR] Failed to write security event to database: %v", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// writeToFile writes the event as JSON to the log file
|
|
func (sl *SecurityLogger) writeToFile(event *models.SecurityEvent) error {
|
|
jsonData, err := json.Marshal(event)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal security event: %w", err)
|
|
}
|
|
|
|
sl.logger.Println(string(jsonData))
|
|
return nil
|
|
}
|
|
|
|
// writeToDatabase writes the event to the database
|
|
func (sl *SecurityLogger) writeToDatabase(event *models.SecurityEvent) error {
|
|
// Create security_events table if not exists
|
|
if err := sl.ensureSecurityEventsTable(); err != nil {
|
|
return fmt.Errorf("failed to ensure security_events table: %w", err)
|
|
}
|
|
|
|
// Encode details and metadata as JSON
|
|
detailsJSON, _ := json.Marshal(event.Details)
|
|
metadataJSON, _ := json.Marshal(event.Metadata)
|
|
|
|
query := `
|
|
INSERT INTO security_events (timestamp, level, event_type, agent_id, message, trace_id, ip_address, details, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`
|
|
|
|
_, err := sl.db.Exec(query,
|
|
event.Timestamp,
|
|
event.Level,
|
|
event.EventType,
|
|
event.AgentID,
|
|
event.Message,
|
|
event.TraceID,
|
|
event.IPAddress,
|
|
detailsJSON,
|
|
metadataJSON,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// ensureSecurityEventsTable creates the security_events table if it doesn't exist
|
|
func (sl *SecurityLogger) ensureSecurityEventsTable() error {
|
|
query := `
|
|
CREATE TABLE IF NOT EXISTS security_events (
|
|
id SERIAL PRIMARY KEY,
|
|
timestamp TIMESTAMP WITH TIME ZONE NOT NULL,
|
|
level VARCHAR(20) NOT NULL,
|
|
event_type VARCHAR(100) NOT NULL,
|
|
agent_id UUID,
|
|
message TEXT NOT NULL,
|
|
trace_id VARCHAR(100),
|
|
ip_address VARCHAR(100),
|
|
details JSONB,
|
|
metadata JSONB,
|
|
INDEX idx_security_events_timestamp (timestamp),
|
|
INDEX idx_security_events_agent_id (agent_id),
|
|
INDEX idx_security_events_level (level),
|
|
INDEX idx_security_events_event_type (event_type)
|
|
)`
|
|
|
|
_, err := sl.db.Exec(query)
|
|
return err
|
|
}
|
|
|
|
// Close closes the security logger and flushes any pending events
|
|
func (sl *SecurityLogger) Close() error {
|
|
if sl.lumberjack != nil {
|
|
close(sl.stopChan)
|
|
sl.wg.Wait()
|
|
if err := sl.lumberjack.Close(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// shouldLogLevel checks if the event should be logged based on the configured level
|
|
func (sl *SecurityLogger) shouldLogLevel(eventLevel string) bool {
|
|
levels := map[string]int{
|
|
"NONE": 0,
|
|
"ERROR": 1,
|
|
"WARNING": 2,
|
|
"INFO": 3,
|
|
"DEBUG": 4,
|
|
}
|
|
|
|
configLevel := levels[sl.config.Level]
|
|
eventLvl, exists := levels[eventLevel]
|
|
if !exists {
|
|
eventLvl = 2 // Default to WARNING
|
|
}
|
|
|
|
return eventLvl <= configLevel
|
|
}
|
|
|
|
// min returns the minimum of two integers
|
|
func min(a, b int) int {
|
|
if a < b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|