feat(correlation): add include_dest_ports filter + README/arch update (v1.1.12)
- feat: new config directive include_dest_ports ([]int) in correlation section - feat: if non-empty, only events with a matching dst_port are correlated - feat: filtered events are silently ignored (not correlated, not emitted as orphan) - feat: new metric failed_dest_port_filtered tracked in ProcessEvent - feat: DEBUG log 'event excluded by dest port filter: source=A dst_port=22' - test: TestCorrelationService_IncludeDestPorts_AllowedPort - test: TestCorrelationService_IncludeDestPorts_FilteredPort - test: TestCorrelationService_IncludeDestPorts_EmptyAllowsAll - docs(readme): full rewrite to match current code (v1.1.12) - docs(readme): add include_dest_ports section, fix version refs, clean outdated sections - docs(arch): add dest_port_filtering section, failed_dest_port_filtered metric, debug log example - fix(config.example): remove obsolete stdout.level field - chore: bump version to 1.1.12 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@ -98,7 +98,8 @@ type CorrelationConfig struct {
|
||||
Matching MatchingConfig `yaml:"matching"`
|
||||
Buffers BuffersConfig `yaml:"buffers"`
|
||||
TTL TTLConfig `yaml:"ttl"`
|
||||
ExcludeSourceIPs []string `yaml:"exclude_source_ips"` // List of source IPs or CIDR ranges to exclude
|
||||
ExcludeSourceIPs []string `yaml:"exclude_source_ips"` // List of source IPs or CIDR ranges to exclude
|
||||
IncludeDestPorts []int `yaml:"include_dest_ports"` // If non-empty, only correlate events matching these destination ports
|
||||
// Deprecated: Use TimeWindow.Value instead
|
||||
TimeWindowS int `yaml:"time_window_s"`
|
||||
// Deprecated: Use OrphanPolicy.ApacheAlwaysEmit instead
|
||||
@ -351,6 +352,12 @@ func (c *UnixSocketConfig) GetSocketPermissions() os.FileMode {
|
||||
return os.FileMode(perms)
|
||||
}
|
||||
|
||||
// GetIncludeDestPorts returns the list of destination ports allowed for correlation.
|
||||
// An empty list means all ports are allowed.
|
||||
func (c *CorrelationConfig) GetIncludeDestPorts() []int {
|
||||
return c.IncludeDestPorts
|
||||
}
|
||||
|
||||
// GetExcludeSourceIPs returns the list of excluded source IPs or CIDR ranges.
|
||||
func (c *CorrelationConfig) GetExcludeSourceIPs() []string {
|
||||
return c.ExcludeSourceIPs
|
||||
|
||||
@ -42,8 +42,9 @@ type CorrelationConfig struct {
|
||||
MaxHTTPBufferSize int // Maximum events to buffer for source A (HTTP)
|
||||
MaxNetworkBufferSize int // Maximum events to buffer for source B (Network)
|
||||
NetworkTTLS int // TTL in seconds for network events (source B)
|
||||
MatchingMode string // "one_to_one" or "one_to_many" (Keep-Alive)
|
||||
MatchingMode string // "one_to_one" or "one_to_many" (Keep-Alive)
|
||||
ExcludeSourceIPs []string // List of source IPs or CIDR ranges to exclude
|
||||
IncludeDestPorts []int // If non-empty, only correlate events matching these destination ports
|
||||
}
|
||||
|
||||
// pendingOrphan represents an A event waiting to be emitted as orphan.
|
||||
@ -181,6 +182,20 @@ func (s *CorrelationService) isIPExcluded(ip string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// isDestPortFiltered returns true if the event's destination port is NOT in the
|
||||
// include list. When IncludeDestPorts is empty, all ports are allowed.
|
||||
func (s *CorrelationService) isDestPortFiltered(port int) bool {
|
||||
if len(s.config.IncludeDestPorts) == 0 {
|
||||
return false // no filter configured: allow all
|
||||
}
|
||||
for _, p := range s.config.IncludeDestPorts {
|
||||
if p == port {
|
||||
return false // port is in the allow-list
|
||||
}
|
||||
}
|
||||
return true // port not in allow-list: filter out
|
||||
}
|
||||
|
||||
// ProcessEvent processes an incoming event and returns correlated logs if matches are found.
|
||||
func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLog {
|
||||
s.mu.Lock()
|
||||
@ -194,6 +209,14 @@ func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLo
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if destination port is in the allow-list
|
||||
if s.isDestPortFiltered(event.DstPort) {
|
||||
s.logger.Debugf("event excluded by dest port filter: source=%s dst_port=%d",
|
||||
event.Source, event.DstPort)
|
||||
s.metrics.RecordCorrelationFailed("dest_port_filtered")
|
||||
return nil
|
||||
}
|
||||
|
||||
// Record event received
|
||||
s.metrics.RecordEventReceived(string(event.Source))
|
||||
|
||||
|
||||
@ -1317,3 +1317,100 @@ func TestCorrelationService_ApacheEmitDelay_Flush(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func TestCorrelationService_IncludeDestPorts_AllowedPort(t *testing.T) {
|
||||
now := time.Now()
|
||||
mt := &mockTimeProvider{now: now}
|
||||
svc := NewCorrelationService(CorrelationConfig{
|
||||
TimeWindow: 10 * time.Second,
|
||||
ApacheAlwaysEmit: true,
|
||||
MatchingMode: MatchingModeOneToMany,
|
||||
IncludeDestPorts: []int{80, 443},
|
||||
}, mt)
|
||||
|
||||
// B event on allowed port 443
|
||||
bEvent := &NormalizedEvent{
|
||||
Source: SourceB, Timestamp: now,
|
||||
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 443,
|
||||
}
|
||||
results := svc.ProcessEvent(bEvent)
|
||||
if len(results) != 0 {
|
||||
t.Fatalf("expected B buffered (no match yet), got %d results", len(results))
|
||||
}
|
||||
|
||||
// A event on same key, allowed port
|
||||
aEvent := &NormalizedEvent{
|
||||
Source: SourceA, Timestamp: now,
|
||||
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 443,
|
||||
}
|
||||
results = svc.ProcessEvent(aEvent)
|
||||
if len(results) != 1 {
|
||||
t.Fatalf("expected 1 correlation, got %d", len(results))
|
||||
}
|
||||
if !results[0].Correlated {
|
||||
t.Error("expected correlated=true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrelationService_IncludeDestPorts_FilteredPort(t *testing.T) {
|
||||
now := time.Now()
|
||||
mt := &mockTimeProvider{now: now}
|
||||
svc := NewCorrelationService(CorrelationConfig{
|
||||
TimeWindow: 10 * time.Second,
|
||||
ApacheAlwaysEmit: true,
|
||||
MatchingMode: MatchingModeOneToMany,
|
||||
IncludeDestPorts: []int{80, 443},
|
||||
}, mt)
|
||||
|
||||
// A event on port 22 (not in allow-list)
|
||||
aEvent := &NormalizedEvent{
|
||||
Source: SourceA, Timestamp: now,
|
||||
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 22,
|
||||
}
|
||||
results := svc.ProcessEvent(aEvent)
|
||||
if len(results) != 0 {
|
||||
t.Fatalf("expected 0 results (filtered), got %d", len(results))
|
||||
}
|
||||
|
||||
// B event on port 22 (not in allow-list)
|
||||
bEvent := &NormalizedEvent{
|
||||
Source: SourceB, Timestamp: now,
|
||||
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 22,
|
||||
}
|
||||
results = svc.ProcessEvent(bEvent)
|
||||
if len(results) != 0 {
|
||||
t.Fatalf("expected 0 results (filtered), got %d", len(results))
|
||||
}
|
||||
|
||||
// Flush should also return nothing
|
||||
flushed := svc.Flush()
|
||||
if len(flushed) != 0 {
|
||||
t.Errorf("expected 0 flushed events, got %d", len(flushed))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCorrelationService_IncludeDestPorts_EmptyAllowsAll(t *testing.T) {
|
||||
now := time.Now()
|
||||
mt := &mockTimeProvider{now: now}
|
||||
// No IncludeDestPorts = all ports allowed
|
||||
svc := NewCorrelationService(CorrelationConfig{
|
||||
TimeWindow: 10 * time.Second,
|
||||
ApacheAlwaysEmit: true,
|
||||
MatchingMode: MatchingModeOneToMany,
|
||||
}, mt)
|
||||
|
||||
bEvent := &NormalizedEvent{
|
||||
Source: SourceB, Timestamp: now,
|
||||
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 9999,
|
||||
}
|
||||
svc.ProcessEvent(bEvent)
|
||||
|
||||
aEvent := &NormalizedEvent{
|
||||
Source: SourceA, Timestamp: now,
|
||||
SrcIP: "1.2.3.4", SrcPort: 1234, DstPort: 9999,
|
||||
}
|
||||
results := svc.ProcessEvent(aEvent)
|
||||
if len(results) != 1 || !results[0].Correlated {
|
||||
t.Errorf("expected 1 correlation on any port when list is empty, got %d", len(results))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user