projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally. Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
This commit is contained in:
commit
67e26be2d9
838 changed files with 157302 additions and 0 deletions
43
agent/internal/logging/example_integration_test.go
Normal file
43
agent/internal/logging/example_integration_test.go
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
package logging
|
||||
|
||||
// example_integration_test.go — Pre-fix tests for dead example code.
|
||||
//
|
||||
// F-D1-3 LOW: example_integration.go is dead code calling machineid.ID()
|
||||
// directly, bypassing the canonical hashMachineID().
|
||||
//
|
||||
// Run: cd agent && go test ./internal/logging/... -v -run TestExample
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4.1 — Documents dead code exists (F-D1-3)
|
||||
//
|
||||
// Category: PASS-NOW (documents dead code)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestExampleIntegrationFileIsDeadCode(t *testing.T) {
|
||||
// POST-FIX (F-D1-3): File deleted.
|
||||
_, err := os.Stat("example_integration.go")
|
||||
if err == nil {
|
||||
t.Error("[ERROR] [agent] [logging] F-D1-3 NOT FIXED: dead code file still exists")
|
||||
}
|
||||
t.Log("[INFO] [agent] [logging] F-D1-3 FIXED: dead example code deleted")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test 4.2 — File should not exist (assert fix)
|
||||
//
|
||||
// Category: FAIL-NOW / PASS-AFTER-FIX
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestExampleIntegrationFileDoesNotExist(t *testing.T) {
|
||||
// F-D1-3: After fix, delete the file.
|
||||
_, err := os.Stat("example_integration.go")
|
||||
if err == nil {
|
||||
t.Errorf("[ERROR] [agent] [logging] example_integration.go still exists.\n" +
|
||||
"F-D1-3: delete dead example code with incorrect usage patterns.")
|
||||
}
|
||||
}
|
||||
63
agent/internal/logging/process_logger.go
Normal file
63
agent/internal/logging/process_logger.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/constants"
|
||||
)
|
||||
|
||||
var (
|
||||
processLoggerMu sync.Mutex
|
||||
processLoggerFile *os.File
|
||||
)
|
||||
|
||||
// ConfigureProcessLogger routes the standard library logger to the primary
|
||||
// Windows agent log. Linux keeps its existing service/journal behavior.
|
||||
func ConfigureProcessLogger() error {
|
||||
if runtime.GOOS != "windows" {
|
||||
return nil
|
||||
}
|
||||
return ConfigureProcessLoggerFile(constants.GetAgentLogPath(), os.Stderr)
|
||||
}
|
||||
|
||||
// ConfigureProcessLoggerFile wires log.Printf/log.Fatal output to both the
|
||||
// console stream and a durable file. This preserves CLI feedback while making
|
||||
// Windows service diagnostics available without Event Viewer.
|
||||
func ConfigureProcessLoggerFile(logPath string, console io.Writer) error {
|
||||
if logPath == "" {
|
||||
return fmt.Errorf("agent log path is empty")
|
||||
}
|
||||
if console == nil {
|
||||
console = io.Discard
|
||||
}
|
||||
|
||||
logDir := filepath.Dir(logPath)
|
||||
if err := os.MkdirAll(logDir, 0750); err != nil {
|
||||
return fmt.Errorf("failed to create agent log directory %q: %w", logDir, err)
|
||||
}
|
||||
|
||||
file, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0640)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open agent log file %q: %w", logPath, err)
|
||||
}
|
||||
|
||||
processLoggerMu.Lock()
|
||||
defer processLoggerMu.Unlock()
|
||||
|
||||
oldFile := processLoggerFile
|
||||
processLoggerFile = file
|
||||
log.SetFlags(log.LstdFlags | log.LUTC)
|
||||
log.SetOutput(io.MultiWriter(file, console))
|
||||
|
||||
if oldFile != nil {
|
||||
_ = oldFile.Close()
|
||||
}
|
||||
log.Printf("[INFO] [agent] [logging] process_logger_initialized path=%s", logPath)
|
||||
return nil
|
||||
}
|
||||
84
agent/internal/logging/process_logger_test.go
Normal file
84
agent/internal/logging/process_logger_test.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigureProcessLoggerFileWritesFileAndConsole(t *testing.T) {
|
||||
previousOutput := log.Writer()
|
||||
previousFlags := log.Flags()
|
||||
t.Cleanup(func() {
|
||||
processLoggerMu.Lock()
|
||||
if processLoggerFile != nil {
|
||||
_ = processLoggerFile.Close()
|
||||
processLoggerFile = nil
|
||||
}
|
||||
processLoggerMu.Unlock()
|
||||
log.SetOutput(previousOutput)
|
||||
log.SetFlags(previousFlags)
|
||||
})
|
||||
|
||||
logPath := filepath.Join(t.TempDir(), "agent.log")
|
||||
var console bytes.Buffer
|
||||
|
||||
if err := ConfigureProcessLoggerFile(logPath, &console); err != nil {
|
||||
t.Fatalf("ConfigureProcessLoggerFile() error = %v", err)
|
||||
}
|
||||
|
||||
entry := "[INFO] [agent] [logging] process_logger_test"
|
||||
log.Print(entry)
|
||||
|
||||
data, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read log file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), entry) {
|
||||
t.Fatalf("log file missing entry %q: %s", entry, data)
|
||||
}
|
||||
if !strings.Contains(console.String(), entry) {
|
||||
t.Fatalf("console output missing entry %q: %s", entry, console.String())
|
||||
}
|
||||
}
|
||||
|
||||
type failingWriter struct{}
|
||||
|
||||
func (f failingWriter) Write([]byte) (int, error) {
|
||||
return 0, errors.New("invalid handle")
|
||||
}
|
||||
|
||||
func TestConfigureProcessLoggerFileWritesFileWhenConsoleFails(t *testing.T) {
|
||||
previousOutput := log.Writer()
|
||||
previousFlags := log.Flags()
|
||||
t.Cleanup(func() {
|
||||
processLoggerMu.Lock()
|
||||
if processLoggerFile != nil {
|
||||
_ = processLoggerFile.Close()
|
||||
processLoggerFile = nil
|
||||
}
|
||||
processLoggerMu.Unlock()
|
||||
log.SetOutput(previousOutput)
|
||||
log.SetFlags(previousFlags)
|
||||
})
|
||||
|
||||
logPath := filepath.Join(t.TempDir(), "agent.log")
|
||||
if err := ConfigureProcessLoggerFile(logPath, failingWriter{}); err != nil {
|
||||
t.Fatalf("ConfigureProcessLoggerFile() error = %v", err)
|
||||
}
|
||||
|
||||
entry := "[INFO] [agent] [logging] service_console_unavailable"
|
||||
log.Print(entry)
|
||||
|
||||
data, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatalf("read log file: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(data), entry) {
|
||||
t.Fatalf("log file missing entry %q after console write failure: %s", entry, data)
|
||||
}
|
||||
}
|
||||
497
agent/internal/logging/security_logger.go
Normal file
497
agent/internal/logging/security_logger.go
Normal file
|
|
@ -0,0 +1,497 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/config"
|
||||
)
|
||||
|
||||
// SecurityEvent represents a security event on the agent side
|
||||
// This is a simplified version of the server model to avoid circular dependencies
|
||||
type SecurityEvent struct {
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
Level string `json:"level"` // CRITICAL, WARNING, INFO, DEBUG
|
||||
EventType string `json:"event_type"`
|
||||
Message string `json:"message"`
|
||||
Details map[string]interface{} `json:"details,omitempty"`
|
||||
}
|
||||
|
||||
// SecurityLogConfig holds configuration for security logging on the agent
|
||||
type SecurityLogConfig struct {
|
||||
Enabled bool `json:"enabled" env:"REDFLAG_AGENT_SECURITY_LOG_ENABLED" default:"true"`
|
||||
Level string `json:"level" env:"REDFLAG_AGENT_SECURITY_LOG_LEVEL" default:"warning"` // none, error, warn, info, debug
|
||||
LogSuccesses bool `json:"log_successes" env:"REDFLAG_AGENT_SECURITY_LOG_SUCCESSES" default:"false"`
|
||||
FilePath string `json:"file_path" env:"REDFLAG_AGENT_SECURITY_LOG_PATH"` // Relative to agent data directory
|
||||
MaxSizeMB int `json:"max_size_mb" env:"REDFLAG_AGENT_SECURITY_LOG_MAX_SIZE" default:"50"`
|
||||
MaxFiles int `json:"max_files" env:"REDFLAG_AGENT_SECURITY_LOG_MAX_FILES" default:"5"`
|
||||
BatchSize int `json:"batch_size" env:"REDFLAG_AGENT_SECURITY_LOG_BATCH_SIZE" default:"10"`
|
||||
SendToServer bool `json:"send_to_server" env:"REDFLAG_AGENT_SECURITY_LOG_SEND" default:"true"`
|
||||
}
|
||||
|
||||
// SecurityLogger handles security event logging on the agent
|
||||
type SecurityLogger struct {
|
||||
config SecurityLogConfig
|
||||
logger *log.Logger
|
||||
file *os.File
|
||||
mu sync.Mutex
|
||||
buffer []*SecurityEvent
|
||||
fileCursor int
|
||||
flushTimer *time.Timer
|
||||
lastFlush time.Time
|
||||
closed bool
|
||||
}
|
||||
|
||||
// SecurityEventTypes defines all possible security event types on the agent
|
||||
var SecurityEventTypes = struct {
|
||||
CmdSignatureVerificationFailed string
|
||||
CmdSignatureVerificationSuccess string
|
||||
UpdateNonceInvalid string
|
||||
UpdateSignatureVerificationFailed string
|
||||
MachineIDChangeDetected string
|
||||
ConfigTamperingWarning string
|
||||
UnauthorizedCommandAttempt string
|
||||
KeyRotationDetected string
|
||||
}{
|
||||
CmdSignatureVerificationFailed: "CMD_SIGNATURE_VERIFICATION_FAILED",
|
||||
CmdSignatureVerificationSuccess: "CMD_SIGNATURE_VERIFICATION_SUCCESS",
|
||||
UpdateNonceInvalid: "UPDATE_NONCE_INVALID",
|
||||
UpdateSignatureVerificationFailed: "UPDATE_SIGNATURE_VERIFICATION_FAILED",
|
||||
MachineIDChangeDetected: "MACHINE_ID_CHANGE_DETECTED",
|
||||
ConfigTamperingWarning: "CONFIG_TAMPERING_WARNING",
|
||||
UnauthorizedCommandAttempt: "UNAUTHORIZED_COMMAND_ATTEMPT",
|
||||
KeyRotationDetected: "KEY_ROTATION_DETECTED",
|
||||
}
|
||||
|
||||
// NewSecurityLogger creates a new agent security logger
|
||||
func NewSecurityLogger(agentConfig *config.Config, logDir string) (*SecurityLogger, error) {
|
||||
// Create default security log config
|
||||
secConfig := SecurityLogConfig{
|
||||
Enabled: true,
|
||||
Level: "warning",
|
||||
LogSuccesses: false,
|
||||
FilePath: "security.log",
|
||||
MaxSizeMB: 50,
|
||||
MaxFiles: 5,
|
||||
BatchSize: 10,
|
||||
SendToServer: true,
|
||||
}
|
||||
|
||||
// Ensure log directory exists
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create security log directory: %w", err)
|
||||
}
|
||||
|
||||
// Open log file
|
||||
logPath := filepath.Join(logDir, secConfig.FilePath)
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0600)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open security log file: %w", err)
|
||||
}
|
||||
|
||||
logger := &SecurityLogger{
|
||||
config: secConfig,
|
||||
logger: log.New(file, "[SECURITY] ", log.LstdFlags|log.LUTC),
|
||||
file: file,
|
||||
buffer: make([]*SecurityEvent, 0, secConfig.BatchSize),
|
||||
lastFlush: time.Now().UTC(),
|
||||
}
|
||||
|
||||
// Start flush timer
|
||||
logger.flushTimer = time.AfterFunc(30*time.Second, logger.flushBuffer)
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// Log writes a security event
|
||||
func (sl *SecurityLogger) Log(event *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 == SecurityEventTypes.CmdSignatureVerificationSuccess {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Filter by log level
|
||||
if !sl.shouldLogLevel(event.Level) {
|
||||
return nil
|
||||
}
|
||||
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
if sl.closed {
|
||||
return fmt.Errorf("security logger is closed")
|
||||
}
|
||||
|
||||
// Add prefix to distinguish security events
|
||||
event.Message = "SECURITY: " + event.Message
|
||||
|
||||
criticalWritten := false
|
||||
if event.Level == "CRITICAL" {
|
||||
if err := sl.writeEvent(event); err != nil {
|
||||
log.Printf("[ERROR] [agent] [security] critical_event_write_failed error=%v", err)
|
||||
} else {
|
||||
criticalWritten = true
|
||||
}
|
||||
}
|
||||
|
||||
// Add to buffer for eventual server delivery. File flushes advance
|
||||
// fileCursor; server delivery clears only after a successful POST.
|
||||
sl.buffer = append(sl.buffer, event)
|
||||
if criticalWritten && sl.fileCursor == len(sl.buffer)-1 {
|
||||
sl.fileCursor++
|
||||
}
|
||||
if len(sl.buffer) >= sl.config.BatchSize {
|
||||
sl.flushBufferUnsafe()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LogCommandVerificationFailure logs a command signature verification failure
|
||||
func (sl *SecurityLogger) LogCommandVerificationFailure(commandID string, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "CRITICAL",
|
||||
EventType: SecurityEventTypes.CmdSignatureVerificationFailed,
|
||||
Message: "Command signature verification failed",
|
||||
Details: map[string]interface{}{
|
||||
"command_id": commandID,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogNonceValidationFailure logs a nonce validation failure
|
||||
func (sl *SecurityLogger) LogNonceValidationFailure(nonce string, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "WARNING",
|
||||
EventType: SecurityEventTypes.UpdateNonceInvalid,
|
||||
Message: "Update nonce validation failed",
|
||||
Details: map[string]interface{}{
|
||||
"nonce": nonce[:min(len(nonce), 16)] + "...", // Truncate for security
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogUpdateSignatureVerificationFailure logs an update signature verification failure
|
||||
func (sl *SecurityLogger) LogUpdateSignatureVerificationFailure(updateID string, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "CRITICAL",
|
||||
EventType: SecurityEventTypes.UpdateSignatureVerificationFailed,
|
||||
Message: "Update signature verification failed",
|
||||
Details: map[string]interface{}{
|
||||
"update_id": updateID,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogMachineIDChangeDetected logs when machine ID changes
|
||||
func (sl *SecurityLogger) LogMachineIDChangeDetected(oldID, newID string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "WARNING",
|
||||
EventType: SecurityEventTypes.MachineIDChangeDetected,
|
||||
Message: "Machine ID change detected",
|
||||
Details: map[string]interface{}{
|
||||
"old_machine_id": oldID,
|
||||
"new_machine_id": newID,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogConfigTamperingWarning logs when configuration tampering is suspected
|
||||
func (sl *SecurityLogger) LogConfigTamperingWarning(configPath string, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "WARNING",
|
||||
EventType: SecurityEventTypes.ConfigTamperingWarning,
|
||||
Message: "Configuration file tampering detected",
|
||||
Details: map[string]interface{}{
|
||||
"config_file": configPath,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogUnauthorizedCommandAttempt logs an attempt to run an unauthorized command
|
||||
func (sl *SecurityLogger) LogUnauthorizedCommandAttempt(command string, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "WARNING",
|
||||
EventType: SecurityEventTypes.UnauthorizedCommandAttempt,
|
||||
Message: "Unauthorized command execution attempt",
|
||||
Details: map[string]interface{}{
|
||||
"command": command,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogCommandVerificationSuccess logs a successful command signature verification
|
||||
func (sl *SecurityLogger) LogCommandVerificationSuccess(commandID string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "INFO",
|
||||
EventType: SecurityEventTypes.CmdSignatureVerificationSuccess,
|
||||
Message: "Command signature verified successfully",
|
||||
Details: map[string]interface{}{
|
||||
"command_id": commandID,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogCommandVerificationFailed logs a failed command signature verification
|
||||
func (sl *SecurityLogger) LogCommandVerificationFailed(commandID, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "CRITICAL",
|
||||
EventType: SecurityEventTypes.CmdSignatureVerificationFailed,
|
||||
Message: "Command signature verification failed",
|
||||
Details: map[string]interface{}{
|
||||
"command_id": commandID,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogKeyRotationDetected logs when a new signing key is detected and cached.
|
||||
// This occurs when a command arrives with a key_id not previously cached by the agent,
|
||||
// indicating the server has rotated its signing key.
|
||||
func (sl *SecurityLogger) LogKeyRotationDetected(keyID string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "INFO",
|
||||
EventType: SecurityEventTypes.KeyRotationDetected,
|
||||
Message: "New signing key detected and cached",
|
||||
Details: map[string]interface{}{
|
||||
"key_id": keyID,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// LogCommandSkipped logs when a command is skipped due to signing configuration
|
||||
func (sl *SecurityLogger) LogCommandSkipped(commandID, reason string) {
|
||||
if sl == nil {
|
||||
return
|
||||
}
|
||||
|
||||
event := &SecurityEvent{
|
||||
Timestamp: time.Now().UTC(),
|
||||
Level: "INFO",
|
||||
EventType: "COMMAND_SKIPPED",
|
||||
Message: "Command skipped due to signing configuration",
|
||||
Details: map[string]interface{}{
|
||||
"command_id": commandID,
|
||||
"reason": reason,
|
||||
},
|
||||
}
|
||||
|
||||
_ = sl.Log(event)
|
||||
}
|
||||
|
||||
// GetBatch returns a batch of events for sending to server
|
||||
func (sl *SecurityLogger) GetBatch() []*SecurityEvent {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
if len(sl.buffer) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
batch := make([]*SecurityEvent, len(sl.buffer))
|
||||
copy(batch, sl.buffer)
|
||||
|
||||
return batch
|
||||
}
|
||||
|
||||
// ClearBatch removes the oldest events after successful server delivery.
|
||||
func (sl *SecurityLogger) ClearBatch(count int) {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
if count <= 0 {
|
||||
return
|
||||
}
|
||||
if count >= len(sl.buffer) {
|
||||
for i := range sl.buffer {
|
||||
sl.buffer[i] = nil
|
||||
}
|
||||
sl.buffer = sl.buffer[:0]
|
||||
sl.fileCursor = 0
|
||||
return
|
||||
}
|
||||
copy(sl.buffer, sl.buffer[count:])
|
||||
for i := len(sl.buffer) - count; i < len(sl.buffer); i++ {
|
||||
sl.buffer[i] = nil
|
||||
}
|
||||
sl.buffer = sl.buffer[:len(sl.buffer)-count]
|
||||
sl.fileCursor -= count
|
||||
if sl.fileCursor < 0 {
|
||||
sl.fileCursor = 0
|
||||
}
|
||||
}
|
||||
|
||||
// writeEvent writes an event to the log file
|
||||
func (sl *SecurityLogger) writeEvent(event *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
|
||||
}
|
||||
|
||||
// flushBuffer flushes all buffered events to file
|
||||
func (sl *SecurityLogger) flushBuffer() {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
sl.flushBufferUnsafe()
|
||||
}
|
||||
|
||||
// flushBufferUnsafe flushes buffer without acquiring lock (must be called with lock held)
|
||||
func (sl *SecurityLogger) flushBufferUnsafe() {
|
||||
if sl.fileCursor > len(sl.buffer) {
|
||||
sl.fileCursor = 0
|
||||
}
|
||||
for sl.fileCursor < len(sl.buffer) {
|
||||
event := sl.buffer[sl.fileCursor]
|
||||
if err := sl.writeEvent(event); err != nil {
|
||||
log.Printf("[ERROR] [agent] [security] security_event_write_failed error=%v", err)
|
||||
break
|
||||
}
|
||||
sl.fileCursor++
|
||||
}
|
||||
|
||||
if sl.fileCursor == len(sl.buffer) {
|
||||
sl.lastFlush = time.Now().UTC()
|
||||
}
|
||||
|
||||
// Reset timer if not closed
|
||||
if !sl.closed && sl.flushTimer != nil {
|
||||
sl.flushTimer.Stop()
|
||||
sl.flushTimer.Reset(30 * time.Second)
|
||||
}
|
||||
}
|
||||
|
||||
// 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[strings.ToUpper(sl.config.Level)]
|
||||
eventLvl, exists := levels[eventLevel]
|
||||
if !exists {
|
||||
eventLvl = 2 // Default to WARNING
|
||||
}
|
||||
|
||||
return eventLvl <= configLevel
|
||||
}
|
||||
|
||||
// Close closes the security logger
|
||||
func (sl *SecurityLogger) Close() error {
|
||||
sl.mu.Lock()
|
||||
defer sl.mu.Unlock()
|
||||
|
||||
if sl.closed {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop flush timer
|
||||
if sl.flushTimer != nil {
|
||||
sl.flushTimer.Stop()
|
||||
}
|
||||
|
||||
// Flush remaining events
|
||||
sl.flushBufferUnsafe()
|
||||
|
||||
// Close file
|
||||
if sl.file != nil {
|
||||
err := sl.file.Close()
|
||||
sl.closed = true
|
||||
return err
|
||||
}
|
||||
|
||||
sl.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
// min returns the minimum of two integers
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
63
agent/internal/logging/security_logger_test.go
Normal file
63
agent/internal/logging/security_logger_test.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
package logging
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/Fimeg/RedFlag/agent/internal/config"
|
||||
)
|
||||
|
||||
func TestSecurityLoggerGetBatchDoesNotClearUntilSuccessfulSend(t *testing.T) {
|
||||
sl, err := NewSecurityLogger(&config.Config{}, t.TempDir())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sl.Close()
|
||||
|
||||
if err := sl.Log(&SecurityEvent{Level: "WARNING", EventType: "TEST_EVENT", Message: "first"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
first := sl.GetBatch()
|
||||
if len(first) != 1 {
|
||||
t.Fatalf("first batch len = %d, want 1", len(first))
|
||||
}
|
||||
second := sl.GetBatch()
|
||||
if len(second) != 1 {
|
||||
t.Fatalf("second batch len = %d, want retained event before ClearBatch", len(second))
|
||||
}
|
||||
|
||||
sl.ClearBatch(len(first))
|
||||
if got := sl.GetBatch(); len(got) != 0 {
|
||||
t.Fatalf("batch len after ClearBatch = %d, want 0", len(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestSecurityLoggerFlushDoesNotDrainServerBufferOrDuplicateFileWrites(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
sl, err := NewSecurityLogger(&config.Config{}, dir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer sl.Close()
|
||||
|
||||
if err := sl.Log(&SecurityEvent{Level: "WARNING", EventType: "TEST_EVENT", Message: "first"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
sl.flushBuffer()
|
||||
sl.flushBuffer()
|
||||
|
||||
if got := sl.GetBatch(); len(got) != 1 {
|
||||
t.Fatalf("batch len after file flush = %d, want 1", len(got))
|
||||
}
|
||||
|
||||
body, err := os.ReadFile(filepath.Join(dir, "security.log"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if count := strings.Count(string(body), "TEST_EVENT"); count != 1 {
|
||||
t.Fatalf("file event count = %d, want 1\n%s", count, string(body))
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue