#!/usr/bin/env python3 """ test-correlation-advanced.py - Advanced correlation testing tool This script provides comprehensive testing for the logcorrelator service, including various scenarios to debug correlation issues. Usage: python3 test-correlation-advanced.py [options] Requirements: - Python 3.6+ - requests library (for metrics): pip install requests """ import argparse import json import socket import sys import time from datetime import datetime from typing import Dict, Any, Optional, Tuple try: import requests HAS_REQUESTS = True except ImportError: HAS_REQUESTS = False class Colors: """ANSI color codes for terminal output.""" BLUE = '\033[0;34m' GREEN = '\033[0;32m' YELLOW = '\033[1;33m' RED = '\033[0;31m' NC = '\033[0m' # No Color BOLD = '\033[1m' def colorize(text: str, color: str) -> str: """Wrap text with ANSI color codes.""" return f"{color}{text}{Colors.NC}" def info(text: str): print(colorize(f"[INFO] ", Colors.BLUE) + text) def success(text: str): print(colorize(f"[OK] ", Colors.GREEN) + text) def warn(text: str): print(colorize(f"[WARN] ", Colors.YELLOW) + text) def error(text: str): print(colorize(f"[ERROR] ", Colors.RED) + text) def debug(text: str, verbose: bool = False): if verbose: print(colorize(f"[DEBUG] ", Colors.BLUE) + text) class CorrelationTester: """Main test class for correlation testing.""" def __init__( self, http_socket: str = "/var/run/logcorrelator/http.socket", network_socket: str = "/var/run/logcorrelator/network.socket", metrics_url: str = "http://localhost:8080/metrics", verbose: bool = False, skip_metrics: bool = False ): self.http_socket = http_socket self.network_socket = network_socket self.metrics_url = metrics_url self.verbose = verbose self.skip_metrics = skip_metrics self.http_sock: Optional[socket.socket] = None self.network_sock: Optional[socket.socket] = None def connect(self) -> bool: """Connect to Unix sockets.""" try: # HTTP socket self.http_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self.http_sock.connect(self.http_socket) debug(f"Connected to HTTP socket: {self.http_socket}", self.verbose) # Network socket self.network_sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM) self.network_sock.connect(self.network_socket) debug(f"Connected to Network socket: {self.network_socket}", self.verbose) return True except FileNotFoundError as e: error(f"Socket not found: {e}") return False except Exception as e: error(f"Connection error: {e}") return False def close(self): """Close socket connections.""" if self.http_sock: self.http_sock.close() if self.network_sock: self.network_sock.close() def send_http_event( self, src_ip: str, src_port: int, timestamp: int, method: str = "GET", path: str = "/test", host: str = "example.com", extra_headers: Optional[Dict[str, str]] = None ) -> Dict[str, Any]: """Send an HTTP (source A) event.""" event = { "src_ip": src_ip, "src_port": src_port, "dst_ip": "10.0.0.1", "dst_port": 443, "timestamp": timestamp, "method": method, "path": path, "host": host, "http_version": "HTTP/1.1", "header_user_agent": "TestAgent/1.0", "header_accept": "*/*" } if extra_headers: for key, value in extra_headers.items(): event[f"header_{key}"] = value json_data = json.dumps(event) if self.http_sock: self.http_sock.sendall(json_data.encode()) debug(f"Sent HTTP event: {src_ip}:{src_port} ts={timestamp}", self.verbose) return event def send_network_event( self, src_ip: str, src_port: int, timestamp: int, ja3: str = "abc123", ja4: str = "def456", tls_version: str = "TLS1.3", tls_sni: str = "example.com" ) -> Dict[str, Any]: """Send a Network (source B) event.""" event = { "src_ip": src_ip, "src_port": src_port, "dst_ip": "10.0.0.1", "dst_port": 443, "timestamp": timestamp, "ja3": ja3, "ja4": ja4, "tls_version": tls_version, "tls_sni": tls_sni } json_data = json.dumps(event) if self.network_sock: self.network_sock.sendall(json_data.encode()) debug(f"Sent Network event: {src_ip}:{src_port} ts={timestamp}", self.verbose) return event def get_metrics(self) -> Dict[str, Any]: """Fetch metrics from the metrics server.""" if self.skip_metrics: return {} if not HAS_REQUESTS: warn("requests library not installed, skipping metrics") return {} try: response = requests.get(self.metrics_url, timeout=5) response.raise_for_status() return response.json() except Exception as e: warn(f"Failed to fetch metrics: {e}") return {} def print_metrics(self, metrics: Dict[str, Any], title: str = "Metrics"): """Print metrics in a formatted way.""" if not metrics: return print(f"\n{colorize(f'=== {title} ===', Colors.BOLD)}") keys_to_show = [ ("events_received_a", "Events A"), ("events_received_b", "Events B"), ("correlations_success", "Correlations"), ("correlations_failed", "Failures"), ("failed_no_match_key", " - No match key"), ("failed_time_window", " - Time window"), ("failed_buffer_eviction", " - Buffer eviction"), ("failed_ttl_expired", " - TTL expired"), ("buffer_a_size", "Buffer A size"), ("buffer_b_size", "Buffer B size"), ("orphans_emitted_a", "Orphans A"), ("orphans_emitted_b", "Orphans B"), ("pending_orphan_match", "Pending orphan matches"), ("keepalive_resets", "Keep-Alive resets"), ] for key, label in keys_to_show: if key in metrics: print(f" {label}: {metrics[key]}") def check_sockets(self) -> bool: """Check if sockets exist.""" import os errors = 0 for name, path in [("HTTP", self.http_socket), ("Network", self.network_socket)]: if not os.path.exists(path): error(f"{name} socket not found: {path}") errors += 1 elif not os.path.exists(path) or not os.path.stat(path).st_mode & 0o170000 == 0o140000: # Check if it's a socket try: if not socket.getaddrinfo(path, None, socket.AF_UNIX): error(f"{name} path exists but is not a socket: {path}") errors += 1 except: pass else: debug(f"{name} socket found: {path}", self.verbose) return errors == 0 def run_basic_test(self, count: int = 10, delay_ms: int = 100) -> Tuple[bool, Dict[str, int]]: """ Run basic correlation test. Sends N pairs of A+B events with matching src_ip:src_port and timestamps. All should correlate successfully. """ info(f"Running basic correlation test with {count} pairs...") # Get initial metrics initial_metrics = self.get_metrics() self.print_metrics(initial_metrics, "Initial Metrics") initial_success = initial_metrics.get("correlations_success", 0) initial_failed = initial_metrics.get("correlations_failed", 0) initial_a = initial_metrics.get("events_received_a", 0) initial_b = initial_metrics.get("events_received_b", 0) # Send test events print(f"\nSending {count} event pairs...") base_timestamp = time.time_ns() sent = 0 for i in range(1, count + 1): src_ip = f"192.168.1.{(i % 254) + 1}" src_port = 8000 + i # Same timestamp for perfect correlation timestamp = base_timestamp + (i * 1_000_000) self.send_http_event(src_ip, src_port, timestamp) self.send_network_event(src_ip, src_port, timestamp) sent += 1 if delay_ms > 0: time.sleep(delay_ms / 1000.0) success(f"Sent {sent} event pairs") # Wait for processing info("Waiting for processing (2 seconds)...") time.sleep(2) # Get final metrics final_metrics = self.get_metrics() self.print_metrics(final_metrics, "Final Metrics") # Calculate deltas delta_success = final_metrics.get("correlations_success", 0) - initial_success delta_failed = final_metrics.get("correlations_failed", 0) - initial_failed delta_a = final_metrics.get("events_received_a", 0) - initial_a delta_b = final_metrics.get("events_received_b", 0) - initial_b results = { "sent": sent, "received_a": delta_a, "received_b": delta_b, "correlations": delta_success, "failures": delta_failed } # Print results print(f"\n{colorize('=== Results ===', Colors.BOLD)}") print(f" Events A sent: {delta_a} (expected: {sent})") print(f" Events B sent: {delta_b} (expected: {sent})") print(f" Correlations: {delta_success}") print(f" Failures: {delta_failed}") # Validation test_passed = True if delta_a != sent: error(f"Event A count mismatch: got {delta_a}, expected {sent}") test_passed = False if delta_b != sent: error(f"Event B count mismatch: got {delta_b}, expected {sent}") test_passed = False if delta_success != sent: error(f"Correlation count mismatch: got {delta_success}, expected {sent}") test_passed = False if delta_failed > 0: warn(f"Unexpected correlation failures: {delta_failed}") if test_passed: success("All tests passed! Correlation is working correctly.") else: error("Some tests failed. Check logs for details.") return test_passed, results def run_time_window_test(self) -> bool: """Test time window expiration.""" info("Running time window test...") src_ip = "192.168.100.1" src_port = 9999 # Send A event ts_a = time.time_ns() self.send_http_event(src_ip, src_port, ts_a) info(f"Sent A event at {ts_a}") # Wait for time window to expire (default 10s) info("Waiting 11 seconds (time window should expire)...") time.sleep(11) # Send B event ts_b = time.time_ns() self.send_network_event(src_ip, src_port, ts_b) info(f"Sent B event at {ts_b}") time_diff_sec = (ts_b - ts_a) / 1_000_000_000 info(f"Time difference: {time_diff_sec:.1f} seconds") info("Expected: time_window failure (check metrics)") return True def run_different_ip_test(self) -> bool: """Test different IP (should not correlate).""" info("Running different IP test...") ts = time.time_ns() # Send A with IP 192.168.200.1 self.send_http_event("192.168.200.1", 7777, ts) info("Sent A event from 192.168.200.1:7777") # Send B with different IP self.send_network_event("192.168.200.2", 7777, ts) info("Sent B event from 192.168.200.2:7777 (different IP)") info("Expected: no_match_key failure (different src_ip)") return True def run_keepalive_test(self, count: int = 5) -> bool: """Test Keep-Alive mode (one B correlates with multiple A).""" info(f"Running Keep-Alive test with {count} HTTP requests on same connection...") src_ip = "192.168.50.1" src_port = 6000 # Send one B event first (network/TCP connection) ts_b = time.time_ns() self.send_network_event(src_ip, src_port, ts_b) info(f"Sent B event (connection): {src_ip}:{src_port}") # Send multiple A events (HTTP requests) on same connection for i in range(count): ts_a = time.time_ns() + (i * 100_000_000) # 100ms apart self.send_http_event(src_ip, src_port, ts_a, path=f"/request{i}") info(f"Sent A event (request {i}): {src_ip}:{src_port}") time.sleep(0.05) # 50ms delay time.sleep(2) # Wait for processing # Check metrics metrics = self.get_metrics() keepalive_resets = metrics.get("keepalive_resets", 0) info(f"Keep-Alive resets: {keepalive_resets} (expected: {count - 1})") if keepalive_resets >= count - 1: success("Keep-Alive test passed!") return True else: warn(f"Keep-Alive resets lower than expected. This may be normal depending on timing.") return True def run_all_tests(self) -> bool: """Run all test scenarios.""" results = [] # Basic test passed, _ = self.run_basic_test(count=10) results.append(("Basic correlation", passed)) print("\n" + "=" * 50 + "\n") # Time window test self.run_time_window_test() results.append(("Time window", True)) # Informational print("\n" + "=" * 50 + "\n") # Different IP test self.run_different_ip_test() results.append(("Different IP", True)) # Informational print("\n" + "=" * 50 + "\n") # Keep-Alive test self.run_keepalive_test() results.append(("Keep-Alive", True)) # Summary print(f"\n{colorize('=== Test Summary ===', Colors.BOLD)}") for name, passed in results: status = colorize("PASS", Colors.GREEN) if passed else colorize("FAIL", Colors.RED) print(f" {name}: {status}") return all(r[1] for r in results) def main(): parser = argparse.ArgumentParser( description="Advanced correlation testing tool for logcorrelator", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: # Run basic test with 20 pairs python3 test-correlation-advanced.py -c 20 # Run all tests with verbose output python3 test-correlation-advanced.py --all -v # Test with custom socket paths python3 test-correlation-advanced.py -H /tmp/http.sock -N /tmp/network.sock # Skip metrics check python3 test-correlation-advanced.py --skip-metrics """ ) parser.add_argument( "-H", "--http-socket", default="/var/run/logcorrelator/http.socket", help="Path to HTTP Unix socket (default: /var/run/logcorrelator/http.socket)" ) parser.add_argument( "-N", "--network-socket", default="/var/run/logcorrelator/network.socket", help="Path to Network Unix socket (default: /var/run/logcorrelator/network.socket)" ) parser.add_argument( "-m", "--metrics-url", default="http://localhost:8080/metrics", help="Metrics server URL (default: http://localhost:8080/metrics)" ) parser.add_argument( "-c", "--count", type=int, default=10, help="Number of test pairs to send (default: 10)" ) parser.add_argument( "-d", "--delay", type=int, default=100, help="Delay between pairs in milliseconds (default: 100)" ) parser.add_argument( "-v", "--verbose", action="store_true", help="Enable verbose output" ) parser.add_argument( "--skip-metrics", action="store_true", help="Skip metrics check" ) parser.add_argument( "--all", action="store_true", help="Run all test scenarios" ) parser.add_argument( "--time-window", action="store_true", help="Run time window test only" ) parser.add_argument( "--different-ip", action="store_true", help="Run different IP test only" ) parser.add_argument( "--keepalive", action="store_true", help="Run Keep-Alive test only" ) args = parser.parse_args() # Create tester tester = CorrelationTester( http_socket=args.http_socket, network_socket=args.network_socket, metrics_url=args.metrics_url, verbose=args.verbose, skip_metrics=args.skip_metrics ) # Check sockets if not tester.check_sockets(): error("Socket check failed. Is logcorrelator running?") sys.exit(1) success("Socket check passed") # Connect if not tester.connect(): error("Failed to connect to sockets") sys.exit(1) try: if args.all: success = tester.run_all_tests() elif args.time_window: tester.run_time_window_test() success = True elif args.different_ip: tester.run_different_ip_test() success = True elif args.keepalive: tester.run_keepalive_test() success = True else: _, _ = tester.run_basic_test(count=args.count, delay_ms=args.delay) success = True sys.exit(0 if success else 1) finally: tester.close() if __name__ == "__main__": main()