fix: renforcer corrélation A/B et sorties stdout/fichier
Co-authored-by: aider (openrouter/openai/gpt-5.3-codex) <aider@aider.chat>
This commit is contained in:
@ -2,6 +2,7 @@ package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
@ -22,7 +23,7 @@ type CorrelatedLog struct {
|
||||
func (c CorrelatedLog) MarshalJSON() ([]byte, error) {
|
||||
// Create a flat map with all fields
|
||||
flat := make(map[string]any)
|
||||
|
||||
|
||||
// Add core fields
|
||||
flat["timestamp"] = c.Timestamp
|
||||
flat["src_ip"] = c.SrcIP
|
||||
@ -37,12 +38,24 @@ func (c CorrelatedLog) MarshalJSON() ([]byte, error) {
|
||||
if c.OrphanSide != "" {
|
||||
flat["orphan_side"] = c.OrphanSide
|
||||
}
|
||||
|
||||
// Merge additional fields
|
||||
|
||||
// Merge additional fields while preserving reserved keys
|
||||
reservedKeys := map[string]struct{}{
|
||||
"timestamp": {},
|
||||
"src_ip": {},
|
||||
"src_port": {},
|
||||
"dst_ip": {},
|
||||
"dst_port": {},
|
||||
"correlated": {},
|
||||
"orphan_side": {},
|
||||
}
|
||||
for k, v := range c.Fields {
|
||||
if _, reserved := reservedKeys[k]; reserved {
|
||||
continue
|
||||
}
|
||||
flat[k] = v
|
||||
}
|
||||
|
||||
|
||||
return json.Marshal(flat)
|
||||
}
|
||||
|
||||
@ -89,13 +102,28 @@ func extractFields(e *NormalizedEvent) map[string]any {
|
||||
|
||||
func mergeFields(a, b *NormalizedEvent) map[string]any {
|
||||
result := make(map[string]any)
|
||||
// Merge fields from both events
|
||||
|
||||
// Start with A fields
|
||||
for k, v := range a.Raw {
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
// Merge B fields with collision handling
|
||||
for k, v := range b.Raw {
|
||||
if existing, exists := result[k]; exists {
|
||||
if reflect.DeepEqual(existing, v) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Collision with different values: keep both with prefixes
|
||||
delete(result, k)
|
||||
result["a_"+k] = existing
|
||||
result["b_"+k] = v
|
||||
continue
|
||||
}
|
||||
result[k] = v
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
@ -96,14 +96,11 @@ func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLo
|
||||
// Check buffer overflow before adding
|
||||
if s.isBufferFull(event.Source) {
|
||||
// Buffer full, drop event or emit as orphan
|
||||
s.logger.Warnf("buffer full, dropping event: source=%s src_ip=%s src_port=%d",
|
||||
s.logger.Warnf("buffer full, dropping event: source=%s src_ip=%s src_port=%d",
|
||||
event.Source, event.SrcIP, event.SrcPort)
|
||||
if event.Source == SourceA && s.config.ApacheAlwaysEmit {
|
||||
return []CorrelatedLog{NewCorrelatedLogFromEvent(event, "A")}
|
||||
}
|
||||
if event.Source == SourceB && s.config.NetworkEmit {
|
||||
return []CorrelatedLog{NewCorrelatedLogFromEvent(event, "B")}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -123,7 +120,7 @@ func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLo
|
||||
|
||||
if shouldBuffer {
|
||||
s.addEvent(event)
|
||||
s.logger.Debugf("event buffered: source=%s src_ip=%s src_port=%d buffer_size=%d",
|
||||
s.logger.Debugf("event buffered: source=%s src_ip=%s src_port=%d buffer_size=%d",
|
||||
event.Source, event.SrcIP, event.SrcPort, s.getBufferSize(event.Source))
|
||||
}
|
||||
|
||||
@ -158,7 +155,7 @@ func (s *CorrelationService) processSourceA(event *NormalizedEvent) ([]Correlate
|
||||
return s.eventsMatch(event, other)
|
||||
}); bEvent != nil {
|
||||
correlated := NewCorrelatedLog(event, bEvent)
|
||||
s.logger.Debugf("correlation found: A(src_ip=%s src_port=%d) + B(src_ip=%s src_port=%d)",
|
||||
s.logger.Debugf("correlation found: A(src_ip=%s src_port=%d) + B(src_ip=%s src_port=%d)",
|
||||
event.SrcIP, event.SrcPort, bEvent.SrcIP, bEvent.SrcPort)
|
||||
return []CorrelatedLog{correlated}, false
|
||||
}
|
||||
@ -182,19 +179,12 @@ func (s *CorrelationService) processSourceB(event *NormalizedEvent) ([]Correlate
|
||||
return s.eventsMatch(other, event)
|
||||
}); aEvent != nil {
|
||||
correlated := NewCorrelatedLog(aEvent, event)
|
||||
s.logger.Debugf("correlation found: A(src_ip=%s src_port=%d) + B(src_ip=%s src_port=%d)",
|
||||
s.logger.Debugf("correlation found: A(src_ip=%s src_port=%d) + B(src_ip=%s src_port=%d)",
|
||||
aEvent.SrcIP, aEvent.SrcPort, event.SrcIP, event.SrcPort)
|
||||
return []CorrelatedLog{correlated}, false
|
||||
}
|
||||
|
||||
// No match found - orphan B event (not emitted by default)
|
||||
if s.config.NetworkEmit {
|
||||
orphan := NewCorrelatedLogFromEvent(event, "B")
|
||||
s.logger.Warnf("orphan B event (no A match): src_ip=%s src_port=%d", event.SrcIP, event.SrcPort)
|
||||
return []CorrelatedLog{orphan}, false
|
||||
}
|
||||
|
||||
// Keep in buffer for potential future match
|
||||
// Never emit B alone. Keep in buffer for potential future match.
|
||||
return nil, true
|
||||
}
|
||||
|
||||
@ -231,20 +221,17 @@ func (s *CorrelationService) cleanExpired() {
|
||||
// cleanBuffer removes expired events from a buffer.
|
||||
func (s *CorrelationService) cleanBuffer(buffer *eventBuffer, pending map[string][]*list.Element, cutoff time.Time) {
|
||||
for elem := buffer.events.Front(); elem != nil; {
|
||||
next := elem.Next()
|
||||
event := elem.Value.(*NormalizedEvent)
|
||||
if event.Timestamp.Before(cutoff) {
|
||||
next := elem.Next()
|
||||
key := event.CorrelationKey()
|
||||
buffer.events.Remove(elem)
|
||||
pending[key] = removeElementFromSlice(pending[key], elem)
|
||||
if len(pending[key]) == 0 {
|
||||
delete(pending, key)
|
||||
}
|
||||
elem = next
|
||||
continue
|
||||
}
|
||||
// Events are inserted in arrival order; once we hit a non-expired event, stop.
|
||||
break
|
||||
elem = next
|
||||
}
|
||||
}
|
||||
|
||||
@ -307,14 +294,7 @@ func (s *CorrelationService) Flush() []CorrelatedLog {
|
||||
}
|
||||
}
|
||||
|
||||
// Emit remaining B events as orphans only if explicitly enabled
|
||||
if s.config.NetworkEmit {
|
||||
for elem := s.bufferB.events.Front(); elem != nil; elem = elem.Next() {
|
||||
event := elem.Value.(*NormalizedEvent)
|
||||
orphan := NewCorrelatedLogFromEvent(event, "B")
|
||||
results = append(results, orphan)
|
||||
}
|
||||
}
|
||||
// Never emit remaining B events alone.
|
||||
|
||||
// Clear buffers
|
||||
s.bufferA.events.Init()
|
||||
|
||||
@ -193,7 +193,6 @@ func TestCorrelationService_FlushWithEvents(t *testing.T) {
|
||||
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
timeProvider := &mockTimeProvider{now: now}
|
||||
|
||||
// Flush only emits events if ApacheAlwaysEmit and NetworkEmit are true
|
||||
config := CorrelationConfig{
|
||||
TimeWindow: time.Second,
|
||||
ApacheAlwaysEmit: true,
|
||||
@ -201,7 +200,6 @@ func TestCorrelationService_FlushWithEvents(t *testing.T) {
|
||||
}
|
||||
svc := NewCorrelationService(config, timeProvider)
|
||||
|
||||
// We need to bypass the normal ProcessEvent logic to get events into buffers
|
||||
// Add events directly to buffers for testing Flush
|
||||
keyA := "192.168.1.1:8080"
|
||||
keyB := "192.168.1.2:9090"
|
||||
@ -219,7 +217,6 @@ func TestCorrelationService_FlushWithEvents(t *testing.T) {
|
||||
SrcPort: 9090,
|
||||
}
|
||||
|
||||
// Manually add to buffers (simulating events that couldn't be matched)
|
||||
elemA := svc.bufferA.events.PushBack(apacheEvent)
|
||||
svc.pendingA[keyA] = append(svc.pendingA[keyA], elemA)
|
||||
|
||||
@ -227,8 +224,11 @@ func TestCorrelationService_FlushWithEvents(t *testing.T) {
|
||||
svc.pendingB[keyB] = append(svc.pendingB[keyB], elemB)
|
||||
|
||||
flushed := svc.Flush()
|
||||
if len(flushed) != 2 {
|
||||
t.Errorf("expected 2 flushed events, got %d", len(flushed))
|
||||
if len(flushed) != 1 {
|
||||
t.Errorf("expected 1 flushed event (A only), got %d", len(flushed))
|
||||
}
|
||||
if len(flushed) == 1 && flushed[0].OrphanSide != "A" {
|
||||
t.Errorf("expected orphan side A, got %s", flushed[0].OrphanSide)
|
||||
}
|
||||
|
||||
// Verify buffers are cleared
|
||||
@ -262,7 +262,7 @@ func TestCorrelationService_BufferOverflow(t *testing.T) {
|
||||
svc.ProcessEvent(event)
|
||||
}
|
||||
|
||||
// Buffer full, next event should be dropped (not emitted since ApacheAlwaysEmit=false but buffer full)
|
||||
// Buffer full, next event should be dropped (not emitted since ApacheAlwaysEmit=false)
|
||||
overflowEvent := &NormalizedEvent{
|
||||
Source: SourceA,
|
||||
Timestamp: now,
|
||||
@ -340,3 +340,33 @@ func TestCorrelationService_DifferentSourceTypes(t *testing.T) {
|
||||
t.Error("expected correlated result")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrelationService_NetworkEmitTrue_DoesNotEmitBAlone(t *testing.T) {
|
||||
now := time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC)
|
||||
timeProvider := &mockTimeProvider{now: now}
|
||||
|
||||
config := CorrelationConfig{
|
||||
TimeWindow: time.Second,
|
||||
ApacheAlwaysEmit: false,
|
||||
NetworkEmit: true,
|
||||
}
|
||||
|
||||
svc := NewCorrelationService(config, timeProvider)
|
||||
|
||||
networkEvent := &NormalizedEvent{
|
||||
Source: SourceB,
|
||||
Timestamp: now,
|
||||
SrcIP: "10.10.10.10",
|
||||
SrcPort: 5555,
|
||||
}
|
||||
|
||||
results := svc.ProcessEvent(networkEvent)
|
||||
if len(results) != 0 {
|
||||
t.Errorf("expected 0 immediate results for orphan B, got %d", len(results))
|
||||
}
|
||||
|
||||
flushed := svc.Flush()
|
||||
if len(flushed) != 0 {
|
||||
t.Errorf("expected 0 flushed orphan B events, got %d", len(flushed))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user