Skip to main content

synapse_pingora/
lib.rs

1//! Synapse-Pingora: High-performance WAF proxy using Cloudflare Pingora.
2//!
3//! This library provides multi-site reverse proxy capabilities with integrated
4//! WAF detection using the Synapse engine.
5//!
6//! # Phase 1 Modules (Core Features)
7//!
8//! - [`vhost`] - Virtual host matching for multi-site routing
9//! - [`config`] - Configuration loading and validation
10//! - [`tls`] - TLS certificate management with SNI support
11//! - [`health`] - Health check endpoint for monitoring
12//! - [`site_waf`] - Per-site WAF configuration management
13//!
14//! # Phase 2 Modules (Management Features)
15//!
16//! - [`metrics`] - Prometheus metrics endpoint
17//! - [`reload`] - Configuration hot-reload via SIGHUP
18//! - [`access`] - CIDR-based allow/deny access lists
19//! - [`ratelimit`] - Per-site rate limiting with token bucket
20//! - [`api`] - Management HTTP API
21//!
22//! # Phase 3 Modules (Feature Migration from risk-server)
23//!
24//! - [`fingerprint`] - JA4/JA4H TLS and HTTP fingerprinting
25//! - [`entity`] - Per-IP entity tracking with risk scoring and decay
26//! - [`tarpit`] - Progressive response delays for slow-drip defense
27//! - [`dlp`] - Data Loss Prevention with 23 sensitive data patterns
28
29// Phase 1: Core Features
30pub mod config;
31pub mod config_manager;
32pub mod health;
33pub mod site_waf;
34pub mod tls;
35pub mod utils;
36pub mod vhost;
37
38// Phase 2: Management Features
39pub mod access;
40pub mod admin_server;
41pub mod api;
42pub mod intelligence;
43pub mod metrics;
44pub mod ratelimit;
45pub mod reload;
46
47// Phase 3: Feature Migration from risk-server
48pub mod dlp;
49pub mod entity;
50pub mod fingerprint;
51pub mod tarpit;
52
53// Phase 6: Security Hardening
54pub mod sni_validation;
55pub mod validation;
56
57// Phase 7: Persistence
58pub mod persistence;
59
60// Phase 3: Telemetry (Alerting)
61pub mod signals;
62pub mod telemetry;
63
64// Phase 3: Honeypot Trap Detection
65pub mod trap;
66
67// Phase 4: Campaign Correlation
68pub mod correlation;
69
70// Phase 5: Actor State Management
71pub mod actor;
72
73// Phase 5: Session State Management
74pub mod session;
75
76// Phase 6: Interrogator System (Progressive Challenge Escalation)
77pub mod interrogator;
78
79// Phase 7: Shadow Mirroring (Honeypot Integration)
80pub mod shadow;
81
82// Phase 8: API Profiler (Behavioral Learning)
83pub mod profiler;
84
85// Phase 9: Risk-Server Port (Payload, Crawler, Trends, Horizon)
86pub mod crawler;
87pub mod horizon;
88pub mod payload;
89pub mod trends;
90pub mod tunnel;
91
92// Phase 10: Libsynapse Consolidation (Geo, WAF Engine, Credential Stuffing)
93pub mod detection;
94pub mod geo;
95pub mod waf;
96
97// Dashboard support
98pub mod block_log;
99pub mod tui;
100
101// Header Manipulation
102pub mod headers;
103
104// Body Inspection
105pub mod body;
106
107// Block Page Rendering
108pub mod block_page;
109
110// Re-export commonly used types from Phase 1
111pub use config::{ConfigFile, ConfigLoader, GlobalConfig};
112pub use health::{HealthChecker, HealthResponse, HealthStatus};
113pub use site_waf::{SiteWafConfig, SiteWafManager, WafAction};
114pub use tls::{TlsManager, TlsVersion};
115pub use vhost::{SiteConfig, VhostMatcher};
116
117// Re-export commonly used types from Phase 2
118pub use access::{AccessDecision, AccessList, AccessListManager};
119pub use api::{ApiHandler, ApiResponse, EvaluateResult};
120pub use metrics::{BandwidthDataPoint, BandwidthStats, MetricsRegistry, ProfilingMetrics};
121pub use ratelimit::{RateLimitConfig, RateLimitDecision, RateLimitManager};
122pub use reload::{ConfigReloader, ReloadResult};
123
124// Re-export commonly used types from Phase 3
125pub use dlp::{
126    validate_credit_card, validate_iban, validate_phone, validate_ssn, DlpConfig, DlpMatch,
127    DlpScanner, DlpStats, PatternSeverity, ScanResult, SensitiveDataType,
128};
129pub use entity::{
130    BlockDecision, EntityConfig, EntityManager, EntityMetrics, EntitySnapshot, EntityState,
131    RiskApplication,
132};
133pub use fingerprint::{
134    analyze_ja4, analyze_ja4h, extract_client_fingerprint, generate_ja4h, parse_ja4_from_header,
135    ClientFingerprint, HttpHeaders, Ja4Analysis, Ja4Fingerprint, Ja4Protocol, Ja4SniType,
136    Ja4hAnalysis, Ja4hFingerprint,
137};
138pub use tarpit::{TarpitConfig, TarpitDecision, TarpitManager, TarpitState, TarpitStats};
139
140// Re-export validation utilities
141pub use validation::{
142    validate_certificate_file, validate_domain_name, validate_private_key_file,
143    validate_tls_config, ValidationError, ValidationResult,
144};
145
146// Re-export SNI validation types (domain fronting prevention)
147pub use sni_validation::{
148    SniValidationConfig, SniValidationMode, SniValidationResult, SniValidator,
149};
150
151// Re-export honeypot trap types
152pub use trap::{TrapConfig, TrapMatcher};
153
154// Re-export dashboard support types
155pub use block_log::{BlockEvent, BlockLog};
156
157// Re-export actor management types
158pub use actor::{ActorConfig, ActorManager, ActorState, ActorStats, RuleMatch};
159
160// Re-export session management types
161pub use session::{
162    HijackAlert, HijackType, SessionConfig, SessionDecision, SessionManager, SessionState,
163    SessionStats,
164};
165
166// Re-export interrogator types
167pub use interrogator::{
168    ActorChallengeState, ChallengeLevel, ChallengeResponse, CookieChallenge, CookieConfig,
169    CookieManager, CookieStats, Interrogator, JsChallenge, JsChallengeConfig, JsChallengeManager,
170    JsChallengeStats, ProgressionConfig, ProgressionManager, ProgressionStats,
171    ValidationResult as ChallengeValidationResult,
172};
173
174// Re-export shadow mirroring types
175pub use shadow::{
176    MirrorPayload, RateLimiter as ShadowRateLimiter, RateLimiterStats as ShadowRateLimiterStats,
177    ShadowClientStats, ShadowMirrorClient, ShadowMirrorConfig, ShadowMirrorError,
178    ShadowMirrorManager, ShadowMirrorStats,
179};
180
181// Re-export profiler types
182pub use profiler::{
183    detect_pattern,
184    entropy_z_score,
185    is_entropy_anomaly,
186    matches_pattern,
187    normalized_entropy,
188    shannon_entropy,
189    AnomalyResult,
190    AnomalySignal,
191    AnomalySignalType,
192    Distribution,
193    EndpointProfile,
194    FieldSchema,
195    FieldType,
196    HeaderAnomaly,
197    HeaderAnomalyResult,
198    HeaderBaseline,
199    // Header profiling types (W4.1 HeaderProfiler)
200    HeaderProfiler,
201    HeaderProfilerStats,
202    JsonEndpointSchema,
203    ParameterSchema,
204    PatternType,
205    PercentilesTracker,
206    ProfileStore,
207    ProfileStoreConfig,
208    ProfileStoreMetrics,
209    Profiler,
210    RateTracker,
211    // Schema learning types (ported from libsynapse)
212    SchemaLearner,
213    SchemaLearnerConfig,
214    SchemaLearnerStats,
215    SchemaViolation,
216    SegmentCardinality,
217    ValueStats,
218    ViolationSeverity,
219    ViolationType,
220};
221
222// Re-export profiler config
223pub use config::ProfilerConfig;
224
225// Re-export crawler detection types
226pub use crawler::{
227    BadBotSeverity, BadBotSignature, CrawlerConfig, CrawlerDefinition, CrawlerDetection,
228    CrawlerDetector, CrawlerStats, CrawlerStatsSnapshot, CrawlerVerificationResult,
229    DnsFailurePolicy, VerificationMethod,
230};
231
232// Re-export Signal Horizon integration types
233pub use horizon::{
234    BlockType, BlocklistCache, BlocklistEntry, BlocklistUpdate, ClientStats, ConnectionState,
235    HorizonClient, HorizonConfig, HorizonError, HorizonManager, HorizonStats, HorizonStatsSnapshot,
236    Severity, SignalType, ThreatSignal,
237};
238
239// Re-export payload profiling types
240pub use payload::{
241    BandwidthBucket, EndpointPayloadStats, EndpointPayloadStatsSnapshot, EndpointSortBy,
242    EntityBandwidth, PayloadAnomaly, PayloadAnomalyMetadata, PayloadAnomalySeverity,
243    PayloadAnomalyType, PayloadConfig, PayloadManager, PayloadSummary, PayloadWindow, SizeStats,
244};
245
246// Re-export trends/signal tracking types
247pub use trends::{
248    Anomaly, AnomalyDetector, AnomalyDetectorConfig, AnomalyMetadata, AnomalyQueryOptions,
249    AnomalySeverity, AnomalyType, BucketSummary, CategorySummary, Correlation, CorrelationEngine,
250    CorrelationMetadata, CorrelationType, Signal, SignalBucket, SignalCategory, SignalExtractor,
251    SignalMetadata, SignalTrend, SignalType as TrendsSignalType, TimeStore, TimeStoreStats,
252    TrendHistogramBucket, TrendQueryOptions, TrendsConfig, TrendsManager, TrendsManagerStats,
253    TrendsStats, TrendsSummary,
254};
255
256// Re-export intelligence signal aggregation types
257pub use intelligence::{
258    Signal as IntelligenceSignal, SignalCategory as IntelligenceSignalCategory, SignalManager,
259    SignalManagerConfig, SignalQueryOptions, SignalSummary as IntelligenceSignalSummary,
260    TopSignalType as IntelligenceTopSignalType,
261};
262
263// Re-export geo/impossible travel types
264pub use geo::{
265    calculate_speed, haversine_distance, is_valid_coordinates, GeoLocation,
266    ImpossibleTravelDetector, LoginEvent, Severity as GeoSeverity, TravelAlert, TravelConfig,
267    TravelStats,
268};
269
270// Re-export WAF engine types (Phase 10)
271pub use waf::{
272    boolean_operands, build_rule_index, get_candidate_rule_indices, method_to_mask, now_ms,
273    repeat_multiplier, Action as WafRuleAction, AnomalyContribution as WafAnomalyContribution,
274    AnomalySignal as WafAnomalySignal, AnomalySignalType as WafAnomalySignalType,
275    AnomalyType as WafAnomalyType, ArgEntry, BlockingMode as WafBlockingMode, CandidateCache,
276    CandidateCacheKey, Engine as WafEngine, EvalContext, Header as WafHeader, IndexedRule,
277    MatchCondition, MatchValue, Request as WafRequest, RiskConfig as WafRiskConfig,
278    RiskContribution as WafRiskContribution, RuleIndex, StateStore, Synapse, Verdict as WafVerdict,
279    WafError, WafRule,
280};
281
282// Re-export credential stuffing detection types (Phase 10)
283pub use detection::{
284    AuthAttempt, AuthMetrics, AuthResult, CredentialStuffingDetector, DistributedAttack,
285    EntityEndpointKey, StuffingConfig, StuffingEvent, StuffingSeverity, StuffingState,
286    StuffingStats, StuffingVerdict, TakeoverAlert,
287};
288
289// ============================================================================
290// Integration Tests: ActorManager + SessionManager Integration
291// ============================================================================
292// These tests verify the wiring between actor/session managers and the pipeline
293// ============================================================================
294
295#[cfg(test)]
296mod actor_session_integration_tests {
297    use super::*;
298    use std::net::IpAddr;
299    use std::sync::Arc;
300
301    // ========================================================================
302    // Test Helpers
303    // ========================================================================
304
305    fn create_test_actor_manager() -> Arc<ActorManager> {
306        Arc::new(ActorManager::new(ActorConfig {
307            max_actors: 1000,
308            decay_interval_secs: 900,
309            correlation_threshold: 0.7,
310            risk_decay_factor: 0.9,
311            max_rule_matches: 100,
312            max_session_ids: 50,
313            enabled: true,
314            max_risk: 100.0,
315            persist_interval_secs: 300,
316            max_fingerprints_per_actor: 20,
317            max_fingerprint_mappings: 500_000,
318        }))
319    }
320
321    fn create_test_session_manager() -> Arc<SessionManager> {
322        Arc::new(SessionManager::new(SessionConfig {
323            max_sessions: 1000,
324            session_ttl_secs: 3600,
325            idle_timeout_secs: 900,
326            cleanup_interval_secs: 300,
327            enable_ja4_binding: true,
328            enable_ip_binding: false,
329            ja4_mismatch_threshold: 1,
330            ip_change_window_secs: 60,
331            max_alerts_per_session: 10,
332            enabled: true,
333        }))
334    }
335
336    fn create_test_ip(last_octet: u8) -> IpAddr {
337        format!("192.168.1.{}", last_octet).parse().unwrap()
338    }
339
340    // ========================================================================
341    // 1. Actor Creation from Request Tests
342    // ========================================================================
343
344    #[test]
345    fn test_request_with_ip_creates_actor() {
346        let actor_manager = create_test_actor_manager();
347        let ip = create_test_ip(100);
348
349        let actor_id = actor_manager.get_or_create_actor(ip, None);
350
351        assert!(!actor_id.is_empty());
352        assert_eq!(actor_manager.len(), 1);
353
354        let actor = actor_manager.get_actor(&actor_id).unwrap();
355        assert!(actor.ips.contains(&ip));
356        assert!(!actor.is_blocked);
357        assert_eq!(actor.risk_score, 0.0);
358    }
359
360    #[test]
361    fn test_request_with_ip_and_fingerprint_creates_actor() {
362        let actor_manager = create_test_actor_manager();
363        let ip = create_test_ip(101);
364        let fingerprint = "t13d1516h2_abc123_ja4hash";
365
366        let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
367
368        let actor = actor_manager.get_actor(&actor_id).unwrap();
369        assert!(actor.ips.contains(&ip));
370        assert!(actor.fingerprints.contains(fingerprint));
371    }
372
373    #[test]
374    fn test_multiple_ips_correlated_via_fingerprint() {
375        let actor_manager = create_test_actor_manager();
376        let ip1 = create_test_ip(1);
377        let ip2 = create_test_ip(2);
378        let ip3 = create_test_ip(3);
379        let shared_fingerprint = "t13d1516h2_shared_fingerprint";
380
381        let actor_id1 = actor_manager.get_or_create_actor(ip1, Some(shared_fingerprint));
382        let actor_id2 = actor_manager.get_or_create_actor(ip2, Some(shared_fingerprint));
383        let actor_id3 = actor_manager.get_or_create_actor(ip3, Some(shared_fingerprint));
384
385        assert_eq!(actor_id1, actor_id2);
386        assert_eq!(actor_id2, actor_id3);
387        assert_eq!(actor_manager.len(), 1);
388
389        let actor = actor_manager.get_actor(&actor_id1).unwrap();
390        assert!(actor.ips.contains(&ip1));
391        assert!(actor.ips.contains(&ip2));
392        assert!(actor.ips.contains(&ip3));
393        assert_eq!(actor.ips.len(), 3);
394    }
395
396    #[test]
397    fn test_same_ip_subsequent_requests_correlate() {
398        let actor_manager = create_test_actor_manager();
399        let ip = create_test_ip(50);
400
401        let actor_id1 = actor_manager.get_or_create_actor(ip, None);
402        let actor_id2 = actor_manager.get_or_create_actor(ip, None);
403        let actor_id3 = actor_manager.get_or_create_actor(ip, None);
404
405        assert_eq!(actor_id1, actor_id2);
406        assert_eq!(actor_id2, actor_id3);
407        assert_eq!(actor_manager.len(), 1);
408    }
409
410    #[test]
411    fn test_different_ips_without_fingerprint_create_separate_actors() {
412        let actor_manager = create_test_actor_manager();
413        let ip1 = create_test_ip(10);
414        let ip2 = create_test_ip(20);
415
416        let actor_id1 = actor_manager.get_or_create_actor(ip1, None);
417        let actor_id2 = actor_manager.get_or_create_actor(ip2, None);
418
419        assert_ne!(actor_id1, actor_id2);
420        assert_eq!(actor_manager.len(), 2);
421    }
422
423    // ========================================================================
424    // 2. Rule Match Recording Tests
425    // ========================================================================
426
427    #[test]
428    fn test_matched_rules_recorded_to_actor_history() {
429        let actor_manager = create_test_actor_manager();
430        let ip = create_test_ip(100);
431
432        let actor_id = actor_manager.get_or_create_actor(ip, None);
433        actor_manager.record_rule_match(&actor_id, "sqli-001", 25.0, "sqli");
434
435        let actor = actor_manager.get_actor(&actor_id).unwrap();
436        assert_eq!(actor.rule_matches.len(), 1);
437        assert_eq!(actor.rule_matches[0].rule_id, "sqli-001");
438        assert_eq!(actor.rule_matches[0].category, "sqli");
439        assert_eq!(actor.rule_matches[0].risk_contribution, 25.0);
440    }
441
442    #[test]
443    fn test_risk_score_accumulates_correctly() {
444        let actor_manager = create_test_actor_manager();
445        let ip = create_test_ip(101);
446
447        let actor_id = actor_manager.get_or_create_actor(ip, None);
448
449        actor_manager.record_rule_match(&actor_id, "sqli-001", 25.0, "sqli");
450        actor_manager.record_rule_match(&actor_id, "xss-001", 20.0, "xss");
451        actor_manager.record_rule_match(&actor_id, "path-001", 15.0, "path_traversal");
452
453        let actor = actor_manager.get_actor(&actor_id).unwrap();
454        assert_eq!(actor.risk_score, 60.0);
455        assert_eq!(actor.rule_matches.len(), 3);
456    }
457
458    #[test]
459    fn test_risk_score_capped_at_max() {
460        let actor_manager = create_test_actor_manager();
461        let ip = create_test_ip(102);
462
463        let actor_id = actor_manager.get_or_create_actor(ip, None);
464
465        for i in 0..15 {
466            actor_manager.record_rule_match(&actor_id, &format!("rule-{}", i), 10.0, "attack");
467        }
468
469        let actor = actor_manager.get_actor(&actor_id).unwrap();
470        assert!(actor.risk_score <= 100.0);
471        assert_eq!(actor.risk_score, 100.0);
472    }
473
474    #[test]
475    fn test_category_mapping_works() {
476        let actor_manager = create_test_actor_manager();
477        let ip = create_test_ip(103);
478
479        let actor_id = actor_manager.get_or_create_actor(ip, None);
480
481        actor_manager.record_rule_match(&actor_id, "rule_940001", 10.0, "sqli");
482        actor_manager.record_rule_match(&actor_id, "rule_941001", 10.0, "xss");
483        actor_manager.record_rule_match(&actor_id, "rule_930001", 10.0, "path_traversal");
484        actor_manager.record_rule_match(&actor_id, "rule_932001", 10.0, "rce");
485        actor_manager.record_rule_match(&actor_id, "rule_913001", 10.0, "scanner");
486
487        let actor = actor_manager.get_actor(&actor_id).unwrap();
488        let categories: Vec<&str> = actor
489            .rule_matches
490            .iter()
491            .map(|m| m.category.as_str())
492            .collect();
493        assert!(categories.contains(&"sqli"));
494        assert!(categories.contains(&"xss"));
495        assert!(categories.contains(&"path_traversal"));
496        assert!(categories.contains(&"rce"));
497        assert!(categories.contains(&"scanner"));
498    }
499
500    // ========================================================================
501    // 3. Actor Blocking Tests
502    // ========================================================================
503
504    #[test]
505    fn test_high_risk_actor_gets_blocked() {
506        let actor_manager = create_test_actor_manager();
507        let ip = create_test_ip(100);
508
509        let actor_id = actor_manager.get_or_create_actor(ip, None);
510        assert!(!actor_manager.is_blocked(&actor_id));
511
512        let blocked = actor_manager.block_actor(&actor_id, "High risk score exceeded threshold");
513
514        assert!(blocked);
515        assert!(actor_manager.is_blocked(&actor_id));
516
517        let actor = actor_manager.get_actor(&actor_id).unwrap();
518        assert!(actor.is_blocked);
519        assert_eq!(
520            actor.block_reason,
521            Some("High risk score exceeded threshold".to_string())
522        );
523        assert!(actor.blocked_since.is_some());
524    }
525
526    #[test]
527    fn test_block_decision_enforced_in_subsequent_requests() {
528        let actor_manager = create_test_actor_manager();
529        let ip = create_test_ip(101);
530        let fingerprint = "blocked_actor_fingerprint";
531
532        let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
533        actor_manager.block_actor(&actor_id, "Malicious activity detected");
534
535        let actor_id2 = actor_manager.get_or_create_actor(ip, Some(fingerprint));
536        assert_eq!(actor_id, actor_id2);
537        assert!(actor_manager.is_blocked(&actor_id2));
538
539        let ip2 = create_test_ip(102);
540        let actor_id3 = actor_manager.get_or_create_actor(ip2, Some(fingerprint));
541        assert_eq!(actor_id, actor_id3);
542        assert!(actor_manager.is_blocked(&actor_id3));
543    }
544
545    #[test]
546    fn test_unblock_actor() {
547        let actor_manager = create_test_actor_manager();
548        let ip = create_test_ip(103);
549
550        let actor_id = actor_manager.get_or_create_actor(ip, None);
551
552        actor_manager.block_actor(&actor_id, "Test block");
553        assert!(actor_manager.is_blocked(&actor_id));
554
555        let unblocked = actor_manager.unblock_actor(&actor_id);
556        assert!(unblocked);
557        assert!(!actor_manager.is_blocked(&actor_id));
558
559        let actor = actor_manager.get_actor(&actor_id).unwrap();
560        assert!(!actor.is_blocked);
561        assert!(actor.block_reason.is_none());
562        assert!(actor.blocked_since.is_none());
563    }
564
565    #[test]
566    fn test_list_blocked_actors() {
567        let actor_manager = create_test_actor_manager();
568
569        for i in 0..10 {
570            let ip = create_test_ip(i);
571            let actor_id = actor_manager.get_or_create_actor(ip, None);
572            if i % 2 == 0 {
573                actor_manager.block_actor(&actor_id, &format!("Blocked actor {}", i));
574            }
575        }
576
577        let blocked = actor_manager.list_blocked_actors();
578        assert_eq!(blocked.len(), 5);
579
580        for actor in blocked {
581            assert!(actor.is_blocked);
582        }
583    }
584
585    #[test]
586    fn test_blocking_updates_statistics() {
587        let actor_manager = create_test_actor_manager();
588        let ip = create_test_ip(105);
589
590        let actor_id = actor_manager.get_or_create_actor(ip, None);
591
592        let stats = actor_manager.stats().snapshot();
593        assert_eq!(stats.blocked_actors, 0);
594
595        actor_manager.block_actor(&actor_id, "Test");
596
597        let stats = actor_manager.stats().snapshot();
598        assert_eq!(stats.blocked_actors, 1);
599
600        actor_manager.unblock_actor(&actor_id);
601
602        let stats = actor_manager.stats().snapshot();
603        assert_eq!(stats.blocked_actors, 0);
604    }
605
606    // ========================================================================
607    // 4. Session Validation Tests
608    // ========================================================================
609
610    #[test]
611    fn test_session_token_extraction_and_validation() {
612        let session_manager = create_test_session_manager();
613        let ip = create_test_ip(100);
614        let token_hash = "abc123def456";
615
616        let decision = session_manager.validate_request(token_hash, ip, None);
617        assert_eq!(decision, SessionDecision::New);
618        assert_eq!(session_manager.len(), 1);
619
620        let decision = session_manager.validate_request(token_hash, ip, None);
621        assert_eq!(decision, SessionDecision::Valid);
622        assert_eq!(session_manager.len(), 1);
623    }
624
625    #[test]
626    fn test_valid_session_passes() {
627        let session_manager = create_test_session_manager();
628        let ip = create_test_ip(101);
629        let token_hash = "valid_session_hash";
630        let ja4 = "t13d1516h2_fingerprint";
631
632        session_manager.create_session(token_hash, ip, Some(ja4));
633
634        let decision = session_manager.validate_request(token_hash, ip, Some(ja4));
635        assert_eq!(decision, SessionDecision::Valid);
636    }
637
638    #[test]
639    fn test_ja4_mismatch_triggers_alert() {
640        let session_manager = create_test_session_manager();
641        let ip = create_test_ip(102);
642        let token_hash = "session_for_hijack_test";
643        let original_ja4 = "t13d1516h2_original_fingerprint";
644        let new_ja4 = "t13d1516h2_different_fingerprint";
645
646        session_manager.create_session(token_hash, ip, Some(original_ja4));
647
648        let decision = session_manager.validate_request(token_hash, ip, Some(new_ja4));
649
650        match decision {
651            SessionDecision::Suspicious(alert) => {
652                assert_eq!(alert.alert_type, HijackType::Ja4Mismatch);
653                assert_eq!(alert.original_value, original_ja4);
654                assert_eq!(alert.new_value, new_ja4);
655                assert!(
656                    alert.confidence >= 0.9,
657                    "JA4 mismatch should have high confidence"
658                );
659            }
660            _ => panic!(
661                "Expected Suspicious decision for JA4 mismatch, got {:?}",
662                decision
663            ),
664        }
665    }
666
667    #[test]
668    fn test_expired_session_detected() {
669        let config = SessionConfig {
670            session_ttl_secs: 0, // Immediate expiration
671            idle_timeout_secs: 3600,
672            ..SessionConfig::default()
673        };
674        let session_manager = Arc::new(SessionManager::new(config));
675        let ip = create_test_ip(103);
676        let token_hash = "expiring_session";
677
678        session_manager.create_session(token_hash, ip, None);
679        std::thread::sleep(std::time::Duration::from_millis(10));
680
681        let decision = session_manager.validate_request(token_hash, ip, None);
682        assert_eq!(decision, SessionDecision::Expired);
683    }
684
685    #[test]
686    fn test_session_request_count_increments() {
687        let session_manager = create_test_session_manager();
688        let ip = create_test_ip(104);
689        let token_hash = "counting_session";
690
691        session_manager.validate_request(token_hash, ip, None); // New
692        session_manager.validate_request(token_hash, ip, None); // Valid
693        session_manager.validate_request(token_hash, ip, None); // Valid
694        session_manager.validate_request(token_hash, ip, None); // Valid
695
696        let session = session_manager.get_session(token_hash).unwrap();
697        assert_eq!(session.request_count, 4);
698    }
699
700    #[test]
701    fn test_first_ja4_binds_to_session() {
702        let session_manager = create_test_session_manager();
703        let ip = create_test_ip(105);
704        let token_hash = "binding_session";
705        let ja4 = "t13d1516h2_bound_fingerprint";
706
707        session_manager.create_session(token_hash, ip, None);
708
709        let session = session_manager.get_session(token_hash).unwrap();
710        assert!(session.bound_ja4.is_none());
711
712        session_manager.validate_request(token_hash, ip, Some(ja4));
713
714        let session = session_manager.get_session(token_hash).unwrap();
715        assert_eq!(session.bound_ja4, Some(ja4.to_string()));
716    }
717
718    #[test]
719    fn test_session_with_no_ja4_binding_allows_any_fingerprint() {
720        let config = SessionConfig {
721            enable_ja4_binding: false,
722            ..SessionConfig::default()
723        };
724        let session_manager = Arc::new(SessionManager::new(config));
725        let ip = create_test_ip(106);
726        let token_hash = "unbound_session";
727
728        session_manager.create_session(token_hash, ip, Some("original_ja4"));
729
730        let decision = session_manager.validate_request(token_hash, ip, Some("different_ja4"));
731        assert_eq!(decision, SessionDecision::Valid);
732    }
733
734    // ========================================================================
735    // 5. Admin API Integration Tests
736    // ========================================================================
737
738    #[test]
739    fn test_get_actors_returns_real_data() {
740        let actor_manager = create_test_actor_manager();
741
742        for i in 0..5 {
743            let ip = create_test_ip(i);
744            let actor_id = actor_manager.get_or_create_actor(ip, None);
745            actor_manager.record_rule_match(&actor_id, &format!("rule-{}", i), 10.0, "test");
746        }
747
748        let api_handler = api::ApiHandler::builder()
749            .actor_manager(Arc::clone(&actor_manager))
750            .build();
751
752        let actors = api_handler.handle_list_actors(10);
753
754        assert_eq!(actors.len(), 5);
755        for actor in &actors {
756            assert!(!actor.actor_id.is_empty());
757            assert_eq!(actor.rule_matches.len(), 1);
758            assert_eq!(actor.risk_score, 10.0);
759        }
760    }
761
762    #[test]
763    fn test_get_sessions_returns_real_data() {
764        let session_manager = create_test_session_manager();
765
766        for i in 0..5 {
767            let ip = create_test_ip(i);
768            let token_hash = format!("session_token_{}", i);
769            session_manager.create_session(&token_hash, ip, Some(&format!("ja4_{}", i)));
770        }
771
772        let api_handler = api::ApiHandler::builder()
773            .session_manager(Arc::clone(&session_manager))
774            .build();
775
776        let sessions = api_handler.handle_list_sessions(10);
777
778        assert_eq!(sessions.len(), 5);
779        for session in &sessions {
780            assert!(!session.session_id.is_empty());
781            assert!(session.session_id.starts_with("sess-"));
782            assert!(session.bound_ja4.is_some());
783        }
784    }
785
786    #[test]
787    fn test_get_actor_stats_returns_real_data() {
788        let actor_manager = create_test_actor_manager();
789
790        for i in 0..10 {
791            let ip = create_test_ip(i);
792            let actor_id = actor_manager.get_or_create_actor(ip, None);
793            actor_manager.record_rule_match(&actor_id, "test-rule", 5.0, "test");
794            if i % 3 == 0 {
795                actor_manager.block_actor(&actor_id, "Test block");
796            }
797        }
798
799        let api_handler = api::ApiHandler::builder()
800            .actor_manager(Arc::clone(&actor_manager))
801            .build();
802
803        let stats = api_handler.handle_actor_stats();
804
805        assert!(stats.is_some());
806        let stats = stats.unwrap();
807        assert_eq!(stats.total_actors, 10);
808        assert_eq!(stats.blocked_actors, 4); // 0, 3, 6, 9
809        assert_eq!(stats.total_created, 10);
810        assert_eq!(stats.total_rule_matches, 10);
811    }
812
813    #[test]
814    fn test_get_session_stats_returns_real_data() {
815        let session_manager = create_test_session_manager();
816
817        for i in 0..5 {
818            let ip = create_test_ip(i);
819            let token_hash = format!("session_{}", i);
820            session_manager.create_session(&token_hash, ip, None);
821        }
822
823        let api_handler = api::ApiHandler::builder()
824            .session_manager(Arc::clone(&session_manager))
825            .build();
826
827        let stats = api_handler.handle_session_stats();
828
829        assert!(stats.is_some());
830        let stats = stats.unwrap();
831        assert_eq!(stats.total_sessions, 5);
832        assert_eq!(stats.active_sessions, 5);
833        assert_eq!(stats.total_created, 5);
834    }
835
836    #[test]
837    fn test_api_handler_without_managers_returns_empty() {
838        let api_handler = api::ApiHandler::builder().build();
839
840        let actors = api_handler.handle_list_actors(10);
841        assert!(actors.is_empty());
842
843        let sessions = api_handler.handle_list_sessions(10);
844        assert!(sessions.is_empty());
845
846        let actor_stats = api_handler.handle_actor_stats();
847        assert!(actor_stats.is_none());
848
849        let session_stats = api_handler.handle_session_stats();
850        assert!(session_stats.is_none());
851    }
852
853    #[test]
854    fn test_list_actors_respects_limit() {
855        let actor_manager = create_test_actor_manager();
856
857        for i in 0..20 {
858            let ip = create_test_ip(i);
859            actor_manager.get_or_create_actor(ip, None);
860        }
861
862        let api_handler = api::ApiHandler::builder()
863            .actor_manager(Arc::clone(&actor_manager))
864            .build();
865
866        let actors = api_handler.handle_list_actors(5);
867        assert_eq!(actors.len(), 5);
868
869        let actors = api_handler.handle_list_actors(100);
870        assert_eq!(actors.len(), 20);
871    }
872
873    // ========================================================================
874    // 6. Combined Actor + Session Integration Tests
875    // ========================================================================
876
877    #[test]
878    fn test_session_bound_to_actor() {
879        let actor_manager = create_test_actor_manager();
880        let session_manager = create_test_session_manager();
881        let ip = create_test_ip(100);
882        let fingerprint = "combined_test_fingerprint";
883        let token_hash = "combined_session_token";
884
885        let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
886        session_manager.create_session(token_hash, ip, Some(fingerprint));
887        session_manager.bind_to_actor(token_hash, &actor_id);
888
889        let session = session_manager.get_session(token_hash).unwrap();
890        assert_eq!(session.actor_id, Some(actor_id.clone()));
891
892        let actor_sessions = session_manager.get_actor_sessions(&actor_id);
893        assert_eq!(actor_sessions.len(), 1);
894        assert_eq!(actor_sessions[0].token_hash, token_hash);
895    }
896
897    #[test]
898    fn test_multi_session_single_actor() {
899        let actor_manager = create_test_actor_manager();
900        let session_manager = create_test_session_manager();
901        let ip = create_test_ip(101);
902        let fingerprint = "multi_session_fingerprint";
903
904        let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
905
906        session_manager.create_session("session_tab_1", ip, Some(fingerprint));
907        session_manager.create_session("session_tab_2", ip, Some(fingerprint));
908        session_manager.create_session("session_mobile", ip, Some(fingerprint));
909
910        session_manager.bind_to_actor("session_tab_1", &actor_id);
911        session_manager.bind_to_actor("session_tab_2", &actor_id);
912        session_manager.bind_to_actor("session_mobile", &actor_id);
913
914        let actor_sessions = session_manager.get_actor_sessions(&actor_id);
915        assert_eq!(actor_sessions.len(), 3);
916    }
917
918    #[test]
919    fn test_blocked_actor_affects_all_sessions() {
920        let actor_manager = create_test_actor_manager();
921        let session_manager = create_test_session_manager();
922        let ip = create_test_ip(102);
923        let fingerprint = "blocked_user_fingerprint";
924
925        let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
926        session_manager.create_session("blocked_user_session_1", ip, Some(fingerprint));
927        session_manager.create_session("blocked_user_session_2", ip, Some(fingerprint));
928        session_manager.bind_to_actor("blocked_user_session_1", &actor_id);
929        session_manager.bind_to_actor("blocked_user_session_2", &actor_id);
930
931        actor_manager.block_actor(&actor_id, "Malicious activity");
932
933        assert!(actor_manager.is_blocked(&actor_id));
934
935        let sessions = session_manager.get_actor_sessions(&actor_id);
936        assert_eq!(sessions.len(), 2);
937
938        let actor_id_check = actor_manager.get_or_create_actor(ip, Some(fingerprint));
939        assert_eq!(actor_id, actor_id_check);
940        assert!(actor_manager.is_blocked(&actor_id_check));
941    }
942
943    #[test]
944    fn test_risk_accumulation_workflow() {
945        let actor_manager = create_test_actor_manager();
946        let ip = create_test_ip(103);
947
948        let actor_id = actor_manager.get_or_create_actor(ip, None);
949
950        actor_manager.record_rule_match(&actor_id, "sqli-001", 30.0, "sqli");
951        let actor = actor_manager.get_actor(&actor_id).unwrap();
952        assert_eq!(actor.risk_score, 30.0);
953        assert!(!actor_manager.is_blocked(&actor_id));
954
955        actor_manager.record_rule_match(&actor_id, "sqli-002", 40.0, "sqli");
956        let actor = actor_manager.get_actor(&actor_id).unwrap();
957        assert_eq!(actor.risk_score, 70.0);
958        assert!(!actor_manager.is_blocked(&actor_id));
959
960        actor_manager.record_rule_match(&actor_id, "xss-001", 20.0, "xss");
961        let actor = actor_manager.get_actor(&actor_id).unwrap();
962        assert_eq!(actor.risk_score, 90.0);
963
964        if actor.risk_score >= 80.0 {
965            actor_manager.block_actor(&actor_id, "Risk threshold exceeded");
966        }
967
968        assert!(actor_manager.is_blocked(&actor_id));
969    }
970}