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:
2
Makefile
2
Makefile
@ -20,7 +20,7 @@ BINARY_NAME=logcorrelator
|
|||||||
DIST_DIR=dist
|
DIST_DIR=dist
|
||||||
|
|
||||||
# Package version
|
# Package version
|
||||||
PKG_VERSION ?= 1.1.11
|
PKG_VERSION ?= 1.1.12
|
||||||
|
|
||||||
# Enable BuildKit for better performance
|
# Enable BuildKit for better performance
|
||||||
export DOCKER_BUILDKIT=1
|
export DOCKER_BUILDKIT=1
|
||||||
|
|||||||
@ -412,6 +412,21 @@ correlation:
|
|||||||
description: >
|
description: >
|
||||||
Stratégie 1‑à‑N : un log réseau peut être utilisé pour plusieurs logs HTTP
|
Stratégie 1‑à‑N : un log réseau peut être utilisé pour plusieurs logs HTTP
|
||||||
successifs tant qu'il n'a pas expiré ni été évincé.
|
successifs tant qu'il n'a pas expiré ni été évincé.
|
||||||
|
ip_filtering:
|
||||||
|
directive: exclude_source_ips
|
||||||
|
description: >
|
||||||
|
Liste d'IPs source (exactes ou plages CIDR) à ignorer silencieusement.
|
||||||
|
Événements non corrélés, non émis en orphelin. Métrique : failed_ip_excluded.
|
||||||
|
dest_port_filtering:
|
||||||
|
directive: include_dest_ports
|
||||||
|
description: >
|
||||||
|
Liste blanche de ports de destination. Si non vide, seuls les événements
|
||||||
|
dont le dst_port est dans la liste participent à la corrélation. Les autres
|
||||||
|
sont silencieusement ignorés (non corrélés, non émis en orphelin).
|
||||||
|
Liste vide = tous les ports autorisés (comportement par défaut).
|
||||||
|
Métrique : failed_dest_port_filtered.
|
||||||
|
example:
|
||||||
|
include_dest_ports: [80, 443, 8080, 8443]
|
||||||
|
|
||||||
schema:
|
schema:
|
||||||
description: >
|
description: >
|
||||||
@ -708,6 +723,8 @@ architecture:
|
|||||||
responsibilities:
|
responsibilities:
|
||||||
- Modèles NormalizedEvent et CorrelatedLog.
|
- Modèles NormalizedEvent et CorrelatedLog.
|
||||||
- CorrelationService (fenêtre, TTL, buffers bornés, one-to-many/Keep-Alive, orphelins).
|
- CorrelationService (fenêtre, TTL, buffers bornés, one-to-many/Keep-Alive, orphelins).
|
||||||
|
- Filtrage par IP source (exclude_source_ips, CIDR).
|
||||||
|
- Filtrage par port destination (include_dest_ports, liste blanche).
|
||||||
- Custom JSON marshaling pour CorrelatedLog (structure plate).
|
- Custom JSON marshaling pour CorrelatedLog (structure plate).
|
||||||
- name: internal/ports
|
- name: internal/ports
|
||||||
type: ports
|
type: ports
|
||||||
@ -849,6 +866,7 @@ observability:
|
|||||||
- "A event has no matching B key in buffer: key=..."
|
- "A event has no matching B key in buffer: key=..."
|
||||||
- "A event has same key as B but outside time window: key=... time_diff=5s window=10s"
|
- "A event has same key as B but outside time window: key=... time_diff=5s window=10s"
|
||||||
- "event excluded by IP filter: source=A src_ip=10.0.0.1 src_port=8080"
|
- "event excluded by IP filter: source=A src_ip=10.0.0.1 src_port=8080"
|
||||||
|
- "event excluded by dest port filter: source=A dst_port=22"
|
||||||
- "TTL reset for B event (Keep-Alive): key=... new_ttl=120s"
|
- "TTL reset for B event (Keep-Alive): key=... new_ttl=120s"
|
||||||
- "[clickhouse] DEBUG batch sent: rows=42 table=correlated_logs_http_network"
|
- "[clickhouse] DEBUG batch sent: rows=42 table=correlated_logs_http_network"
|
||||||
info_logs:
|
info_logs:
|
||||||
@ -877,6 +895,7 @@ observability:
|
|||||||
"failed_buffer_eviction": 5,
|
"failed_buffer_eviction": 5,
|
||||||
"failed_ttl_expired": 12,
|
"failed_ttl_expired": 12,
|
||||||
"failed_ip_excluded": 7,
|
"failed_ip_excluded": 7,
|
||||||
|
"failed_dest_port_filtered": 3,
|
||||||
"buffer_a_size": 23,
|
"buffer_a_size": 23,
|
||||||
"buffer_b_size": 18,
|
"buffer_b_size": 18,
|
||||||
"orphans_emitted_a": 92,
|
"orphans_emitted_a": 92,
|
||||||
@ -900,6 +919,7 @@ observability:
|
|||||||
- failed_buffer_eviction: Buffer plein, événement évincé
|
- failed_buffer_eviction: Buffer plein, événement évincé
|
||||||
- failed_ttl_expired: TTL du événement B expiré
|
- failed_ttl_expired: TTL du événement B expiré
|
||||||
- failed_ip_excluded: Événement exclu par filtre IP (exclude_source_ips)
|
- failed_ip_excluded: Événement exclu par filtre IP (exclude_source_ips)
|
||||||
|
- failed_dest_port_filtered: Événement exclu par filtre port destination (include_dest_ports)
|
||||||
buffers:
|
buffers:
|
||||||
- buffer_a_size: Taille actuelle du buffer HTTP
|
- buffer_a_size: Taille actuelle du buffer HTTP
|
||||||
- buffer_b_size: Taille actuelle du buffer réseau
|
- buffer_b_size: Taille actuelle du buffer réseau
|
||||||
@ -929,6 +949,9 @@ observability:
|
|||||||
- symptom: failed_ip_excluded élevé
|
- symptom: failed_ip_excluded élevé
|
||||||
cause: Traffic depuis des IPs configurées dans exclude_source_ips
|
cause: Traffic depuis des IPs configurées dans exclude_source_ips
|
||||||
solution: Vérifier la configuration, c'est normal si attendu
|
solution: Vérifier la configuration, c'est normal si attendu
|
||||||
|
- symptom: failed_dest_port_filtered élevé
|
||||||
|
cause: Traffic sur des ports non listés dans include_dest_ports
|
||||||
|
solution: Vérifier la configuration include_dest_ports, ou vider la liste pour tout accepter
|
||||||
- symptom: orphans_emitted_a élevé
|
- symptom: orphans_emitted_a élevé
|
||||||
cause: Beaucoup de logs A sans correspondance B
|
cause: Beaucoup de logs A sans correspondance B
|
||||||
solution: Vérifier que la source B envoie bien les événements attendus
|
solution: Vérifier que la source B envoie bien les événements attendus
|
||||||
|
|||||||
@ -115,6 +115,7 @@ func main() {
|
|||||||
NetworkTTLS: cfg.Correlation.GetNetworkTTLS(),
|
NetworkTTLS: cfg.Correlation.GetNetworkTTLS(),
|
||||||
MatchingMode: cfg.Correlation.GetMatchingMode(),
|
MatchingMode: cfg.Correlation.GetMatchingMode(),
|
||||||
ExcludeSourceIPs: cfg.Correlation.GetExcludeSourceIPs(),
|
ExcludeSourceIPs: cfg.Correlation.GetExcludeSourceIPs(),
|
||||||
|
IncludeDestPorts: cfg.Correlation.GetIncludeDestPorts(),
|
||||||
}, &domain.RealTimeProvider{})
|
}, &domain.RealTimeProvider{})
|
||||||
|
|
||||||
// Set logger for correlation service
|
// Set logger for correlation service
|
||||||
|
|||||||
@ -36,7 +36,6 @@ outputs:
|
|||||||
|
|
||||||
stdout:
|
stdout:
|
||||||
enabled: false
|
enabled: false
|
||||||
level: INFO # DEBUG: all logs including orphans, INFO: only correlated, WARN: correlated only, ERROR: none
|
|
||||||
|
|
||||||
correlation:
|
correlation:
|
||||||
# Time window for correlation (A and B must be within this window)
|
# Time window for correlation (A and B must be within this window)
|
||||||
@ -74,6 +73,16 @@ correlation:
|
|||||||
- 172.16.0.0/12 # CIDR range (private network)
|
- 172.16.0.0/12 # CIDR range (private network)
|
||||||
- 10.10.10.0/24 # Another CIDR range
|
- 10.10.10.0/24 # Another CIDR range
|
||||||
|
|
||||||
|
# Restrict correlation to specific destination ports (optional)
|
||||||
|
# If non-empty, only events whose dst_port matches one of these values will be correlated
|
||||||
|
# Events on other ports are silently ignored (not correlated, not emitted as orphans)
|
||||||
|
# Useful to focus on HTTP/HTTPS traffic only and ignore unrelated connections
|
||||||
|
# include_dest_ports:
|
||||||
|
# - 80 # HTTP
|
||||||
|
# - 443 # HTTPS
|
||||||
|
# - 8080 # HTTP alt
|
||||||
|
# - 8443 # HTTPS alt
|
||||||
|
|
||||||
# Metrics server configuration (optional, for debugging/monitoring)
|
# Metrics server configuration (optional, for debugging/monitoring)
|
||||||
metrics:
|
metrics:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|||||||
@ -98,7 +98,8 @@ type CorrelationConfig struct {
|
|||||||
Matching MatchingConfig `yaml:"matching"`
|
Matching MatchingConfig `yaml:"matching"`
|
||||||
Buffers BuffersConfig `yaml:"buffers"`
|
Buffers BuffersConfig `yaml:"buffers"`
|
||||||
TTL TTLConfig `yaml:"ttl"`
|
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
|
// Deprecated: Use TimeWindow.Value instead
|
||||||
TimeWindowS int `yaml:"time_window_s"`
|
TimeWindowS int `yaml:"time_window_s"`
|
||||||
// Deprecated: Use OrphanPolicy.ApacheAlwaysEmit instead
|
// Deprecated: Use OrphanPolicy.ApacheAlwaysEmit instead
|
||||||
@ -351,6 +352,12 @@ func (c *UnixSocketConfig) GetSocketPermissions() os.FileMode {
|
|||||||
return os.FileMode(perms)
|
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.
|
// GetExcludeSourceIPs returns the list of excluded source IPs or CIDR ranges.
|
||||||
func (c *CorrelationConfig) GetExcludeSourceIPs() []string {
|
func (c *CorrelationConfig) GetExcludeSourceIPs() []string {
|
||||||
return c.ExcludeSourceIPs
|
return c.ExcludeSourceIPs
|
||||||
|
|||||||
@ -42,8 +42,9 @@ type CorrelationConfig struct {
|
|||||||
MaxHTTPBufferSize int // Maximum events to buffer for source A (HTTP)
|
MaxHTTPBufferSize int // Maximum events to buffer for source A (HTTP)
|
||||||
MaxNetworkBufferSize int // Maximum events to buffer for source B (Network)
|
MaxNetworkBufferSize int // Maximum events to buffer for source B (Network)
|
||||||
NetworkTTLS int // TTL in seconds for network events (source B)
|
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
|
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.
|
// pendingOrphan represents an A event waiting to be emitted as orphan.
|
||||||
@ -181,6 +182,20 @@ func (s *CorrelationService) isIPExcluded(ip string) bool {
|
|||||||
return false
|
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.
|
// ProcessEvent processes an incoming event and returns correlated logs if matches are found.
|
||||||
func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLog {
|
func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLog {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
@ -194,6 +209,14 @@ func (s *CorrelationService) ProcessEvent(event *NormalizedEvent) []CorrelatedLo
|
|||||||
return nil
|
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
|
// Record event received
|
||||||
s.metrics.RecordEventReceived(string(event.Source))
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -141,6 +141,16 @@ exit 0
|
|||||||
%config(noreplace) /etc/logrotate.d/logcorrelator
|
%config(noreplace) /etc/logrotate.d/logcorrelator
|
||||||
|
|
||||||
%changelog
|
%changelog
|
||||||
|
* Thu Mar 05 2026 logcorrelator <dev@example.com> - 1.1.12-1
|
||||||
|
- Feat: New config directive include_dest_ports - restrict correlation to specific destination ports
|
||||||
|
- Feat: If include_dest_ports is non-empty, events on unlisted ports are silently ignored (not correlated, not emitted as orphan)
|
||||||
|
- Feat: New metric failed_dest_port_filtered for monitoring filtered traffic
|
||||||
|
- Feat: Debug log for filtered events: "event excluded by dest port filter: source=A dst_port=22"
|
||||||
|
- Test: New unit tests for include_dest_ports (allowed port, filtered port, empty=all)
|
||||||
|
- Docs: README.md updated with include_dest_ports section and current version references
|
||||||
|
- Docs: architecture.yml updated with include_dest_ports
|
||||||
|
- Fix: config.example.yml - removed obsolete stdout.level field
|
||||||
|
|
||||||
* Thu Mar 05 2026 logcorrelator <dev@example.com> - 1.1.11-1
|
* Thu Mar 05 2026 logcorrelator <dev@example.com> - 1.1.11-1
|
||||||
- Fix: StdoutSink no longer writes correlated/orphan JSON to stdout
|
- Fix: StdoutSink no longer writes correlated/orphan JSON to stdout
|
||||||
- Fix: stdout sink is now a no-op for data; operational logs go to stderr via logger
|
- Fix: stdout sink is now a no-op for data; operational logs go to stderr via logger
|
||||||
|
|||||||
Reference in New Issue
Block a user