pub mod config;
pub mod config_manager;
pub mod health;
pub mod site_waf;
pub mod tls;
pub mod utils;
pub mod vhost;
pub mod access;
pub mod admin_server;
pub mod api;
pub mod intelligence;
pub mod metrics;
pub mod ratelimit;
pub mod reload;
pub mod dlp;
pub mod entity;
pub mod fingerprint;
pub mod tarpit;
pub mod sni_validation;
pub mod validation;
pub mod persistence;
pub mod signals;
pub mod telemetry;
pub mod trap;
pub mod correlation;
pub mod actor;
pub mod session;
pub mod interrogator;
pub mod shadow;
pub mod profiler;
pub mod crawler;
pub mod horizon;
pub mod payload;
pub mod trends;
pub mod tunnel;
pub mod detection;
pub mod geo;
pub mod waf;
pub mod block_log;
pub mod tui;
pub mod headers;
pub mod body;
pub mod block_page;
pub use config::{ConfigFile, ConfigLoader, GlobalConfig};
pub use health::{HealthChecker, HealthResponse, HealthStatus};
pub use site_waf::{SiteWafConfig, SiteWafManager, WafAction};
pub use tls::{TlsManager, TlsVersion};
pub use vhost::{SiteConfig, VhostMatcher};
pub use access::{AccessDecision, AccessList, AccessListManager};
pub use api::{ApiHandler, ApiResponse, EvaluateResult};
pub use metrics::{BandwidthDataPoint, BandwidthStats, MetricsRegistry, ProfilingMetrics};
pub use ratelimit::{RateLimitConfig, RateLimitDecision, RateLimitManager};
pub use reload::{ConfigReloader, ReloadResult};
pub use dlp::{
validate_credit_card, validate_iban, validate_phone, validate_ssn, DlpConfig, DlpMatch,
DlpScanner, DlpStats, PatternSeverity, ScanResult, SensitiveDataType,
};
pub use entity::{
BlockDecision, EntityConfig, EntityManager, EntityMetrics, EntitySnapshot, EntityState,
RiskApplication,
};
pub use fingerprint::{
analyze_ja4, analyze_ja4h, extract_client_fingerprint, generate_ja4h, parse_ja4_from_header,
ClientFingerprint, HttpHeaders, Ja4Analysis, Ja4Fingerprint, Ja4Protocol, Ja4SniType,
Ja4hAnalysis, Ja4hFingerprint,
};
pub use tarpit::{TarpitConfig, TarpitDecision, TarpitManager, TarpitState, TarpitStats};
pub use validation::{
validate_certificate_file, validate_domain_name, validate_private_key_file,
validate_tls_config, ValidationError, ValidationResult,
};
pub use sni_validation::{
SniValidationConfig, SniValidationMode, SniValidationResult, SniValidator,
};
pub use trap::{TrapConfig, TrapMatcher};
pub use block_log::{BlockEvent, BlockLog};
pub use actor::{ActorConfig, ActorManager, ActorState, ActorStats, RuleMatch};
pub use session::{
HijackAlert, HijackType, SessionConfig, SessionDecision, SessionManager, SessionState,
SessionStats,
};
pub use interrogator::{
ActorChallengeState, ChallengeLevel, ChallengeResponse, CookieChallenge, CookieConfig,
CookieManager, CookieStats, Interrogator, JsChallenge, JsChallengeConfig, JsChallengeManager,
JsChallengeStats, ProgressionConfig, ProgressionManager, ProgressionStats,
ValidationResult as ChallengeValidationResult,
};
pub use shadow::{
MirrorPayload, RateLimiter as ShadowRateLimiter, RateLimiterStats as ShadowRateLimiterStats,
ShadowClientStats, ShadowMirrorClient, ShadowMirrorConfig, ShadowMirrorError,
ShadowMirrorManager, ShadowMirrorStats,
};
pub use profiler::{
detect_pattern,
entropy_z_score,
is_entropy_anomaly,
matches_pattern,
normalized_entropy,
shannon_entropy,
AnomalyResult,
AnomalySignal,
AnomalySignalType,
Distribution,
EndpointProfile,
FieldSchema,
FieldType,
HeaderAnomaly,
HeaderAnomalyResult,
HeaderBaseline,
HeaderProfiler,
HeaderProfilerStats,
JsonEndpointSchema,
ParameterSchema,
PatternType,
PercentilesTracker,
ProfileStore,
ProfileStoreConfig,
ProfileStoreMetrics,
Profiler,
RateTracker,
SchemaLearner,
SchemaLearnerConfig,
SchemaLearnerStats,
SchemaViolation,
SegmentCardinality,
ValueStats,
ViolationSeverity,
ViolationType,
};
pub use config::ProfilerConfig;
pub use crawler::{
BadBotSeverity, BadBotSignature, CrawlerConfig, CrawlerDefinition, CrawlerDetection,
CrawlerDetector, CrawlerStats, CrawlerStatsSnapshot, CrawlerVerificationResult,
DnsFailurePolicy, VerificationMethod,
};
pub use horizon::{
BlockType, BlocklistCache, BlocklistEntry, BlocklistUpdate, ClientStats, ConnectionState,
HorizonClient, HorizonConfig, HorizonError, HorizonManager, HorizonStats, HorizonStatsSnapshot,
Severity, SignalType, ThreatSignal,
};
pub use payload::{
BandwidthBucket, EndpointPayloadStats, EndpointPayloadStatsSnapshot, EndpointSortBy,
EntityBandwidth, PayloadAnomaly, PayloadAnomalyMetadata, PayloadAnomalySeverity,
PayloadAnomalyType, PayloadConfig, PayloadManager, PayloadSummary, PayloadWindow, SizeStats,
};
pub use trends::{
Anomaly, AnomalyDetector, AnomalyDetectorConfig, AnomalyMetadata, AnomalyQueryOptions,
AnomalySeverity, AnomalyType, BucketSummary, CategorySummary, Correlation, CorrelationEngine,
CorrelationMetadata, CorrelationType, Signal, SignalBucket, SignalCategory, SignalExtractor,
SignalMetadata, SignalTrend, SignalType as TrendsSignalType, TimeStore, TimeStoreStats,
TrendHistogramBucket, TrendQueryOptions, TrendsConfig, TrendsManager, TrendsManagerStats,
TrendsStats, TrendsSummary,
};
pub use intelligence::{
Signal as IntelligenceSignal, SignalCategory as IntelligenceSignalCategory, SignalManager,
SignalManagerConfig, SignalQueryOptions, SignalSummary as IntelligenceSignalSummary,
TopSignalType as IntelligenceTopSignalType,
};
pub use geo::{
calculate_speed, haversine_distance, is_valid_coordinates, GeoLocation,
ImpossibleTravelDetector, LoginEvent, Severity as GeoSeverity, TravelAlert, TravelConfig,
TravelStats,
};
pub use waf::{
boolean_operands, build_rule_index, get_candidate_rule_indices, method_to_mask, now_ms,
repeat_multiplier, Action as WafRuleAction, AnomalyContribution as WafAnomalyContribution,
AnomalySignal as WafAnomalySignal, AnomalySignalType as WafAnomalySignalType,
AnomalyType as WafAnomalyType, ArgEntry, BlockingMode as WafBlockingMode, CandidateCache,
CandidateCacheKey, Engine as WafEngine, EvalContext, Header as WafHeader, IndexedRule,
MatchCondition, MatchValue, Request as WafRequest, RiskConfig as WafRiskConfig,
RiskContribution as WafRiskContribution, RuleIndex, StateStore, Synapse, Verdict as WafVerdict,
WafError, WafRule,
};
pub use detection::{
AuthAttempt, AuthMetrics, AuthResult, CredentialStuffingDetector, DistributedAttack,
EntityEndpointKey, StuffingConfig, StuffingEvent, StuffingSeverity, StuffingState,
StuffingStats, StuffingVerdict, TakeoverAlert,
};
#[cfg(test)]
mod actor_session_integration_tests {
use super::*;
use std::net::IpAddr;
use std::sync::Arc;
fn create_test_actor_manager() -> Arc<ActorManager> {
Arc::new(ActorManager::new(ActorConfig {
max_actors: 1000,
decay_interval_secs: 900,
correlation_threshold: 0.7,
risk_decay_factor: 0.9,
max_rule_matches: 100,
max_session_ids: 50,
enabled: true,
max_risk: 100.0,
persist_interval_secs: 300,
max_fingerprints_per_actor: 20,
max_fingerprint_mappings: 500_000,
}))
}
fn create_test_session_manager() -> Arc<SessionManager> {
Arc::new(SessionManager::new(SessionConfig {
max_sessions: 1000,
session_ttl_secs: 3600,
idle_timeout_secs: 900,
cleanup_interval_secs: 300,
enable_ja4_binding: true,
enable_ip_binding: false,
ja4_mismatch_threshold: 1,
ip_change_window_secs: 60,
max_alerts_per_session: 10,
enabled: true,
}))
}
fn create_test_ip(last_octet: u8) -> IpAddr {
format!("192.168.1.{}", last_octet).parse().unwrap()
}
#[test]
fn test_request_with_ip_creates_actor() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(100);
let actor_id = actor_manager.get_or_create_actor(ip, None);
assert!(!actor_id.is_empty());
assert_eq!(actor_manager.len(), 1);
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert!(actor.ips.contains(&ip));
assert!(!actor.is_blocked);
assert_eq!(actor.risk_score, 0.0);
}
#[test]
fn test_request_with_ip_and_fingerprint_creates_actor() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(101);
let fingerprint = "t13d1516h2_abc123_ja4hash";
let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert!(actor.ips.contains(&ip));
assert!(actor.fingerprints.contains(fingerprint));
}
#[test]
fn test_multiple_ips_correlated_via_fingerprint() {
let actor_manager = create_test_actor_manager();
let ip1 = create_test_ip(1);
let ip2 = create_test_ip(2);
let ip3 = create_test_ip(3);
let shared_fingerprint = "t13d1516h2_shared_fingerprint";
let actor_id1 = actor_manager.get_or_create_actor(ip1, Some(shared_fingerprint));
let actor_id2 = actor_manager.get_or_create_actor(ip2, Some(shared_fingerprint));
let actor_id3 = actor_manager.get_or_create_actor(ip3, Some(shared_fingerprint));
assert_eq!(actor_id1, actor_id2);
assert_eq!(actor_id2, actor_id3);
assert_eq!(actor_manager.len(), 1);
let actor = actor_manager.get_actor(&actor_id1).unwrap();
assert!(actor.ips.contains(&ip1));
assert!(actor.ips.contains(&ip2));
assert!(actor.ips.contains(&ip3));
assert_eq!(actor.ips.len(), 3);
}
#[test]
fn test_same_ip_subsequent_requests_correlate() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(50);
let actor_id1 = actor_manager.get_or_create_actor(ip, None);
let actor_id2 = actor_manager.get_or_create_actor(ip, None);
let actor_id3 = actor_manager.get_or_create_actor(ip, None);
assert_eq!(actor_id1, actor_id2);
assert_eq!(actor_id2, actor_id3);
assert_eq!(actor_manager.len(), 1);
}
#[test]
fn test_different_ips_without_fingerprint_create_separate_actors() {
let actor_manager = create_test_actor_manager();
let ip1 = create_test_ip(10);
let ip2 = create_test_ip(20);
let actor_id1 = actor_manager.get_or_create_actor(ip1, None);
let actor_id2 = actor_manager.get_or_create_actor(ip2, None);
assert_ne!(actor_id1, actor_id2);
assert_eq!(actor_manager.len(), 2);
}
#[test]
fn test_matched_rules_recorded_to_actor_history() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(100);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.record_rule_match(&actor_id, "sqli-001", 25.0, "sqli");
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert_eq!(actor.rule_matches.len(), 1);
assert_eq!(actor.rule_matches[0].rule_id, "sqli-001");
assert_eq!(actor.rule_matches[0].category, "sqli");
assert_eq!(actor.rule_matches[0].risk_contribution, 25.0);
}
#[test]
fn test_risk_score_accumulates_correctly() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(101);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.record_rule_match(&actor_id, "sqli-001", 25.0, "sqli");
actor_manager.record_rule_match(&actor_id, "xss-001", 20.0, "xss");
actor_manager.record_rule_match(&actor_id, "path-001", 15.0, "path_traversal");
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert_eq!(actor.risk_score, 60.0);
assert_eq!(actor.rule_matches.len(), 3);
}
#[test]
fn test_risk_score_capped_at_max() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(102);
let actor_id = actor_manager.get_or_create_actor(ip, None);
for i in 0..15 {
actor_manager.record_rule_match(&actor_id, &format!("rule-{}", i), 10.0, "attack");
}
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert!(actor.risk_score <= 100.0);
assert_eq!(actor.risk_score, 100.0);
}
#[test]
fn test_category_mapping_works() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(103);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.record_rule_match(&actor_id, "rule_940001", 10.0, "sqli");
actor_manager.record_rule_match(&actor_id, "rule_941001", 10.0, "xss");
actor_manager.record_rule_match(&actor_id, "rule_930001", 10.0, "path_traversal");
actor_manager.record_rule_match(&actor_id, "rule_932001", 10.0, "rce");
actor_manager.record_rule_match(&actor_id, "rule_913001", 10.0, "scanner");
let actor = actor_manager.get_actor(&actor_id).unwrap();
let categories: Vec<&str> = actor
.rule_matches
.iter()
.map(|m| m.category.as_str())
.collect();
assert!(categories.contains(&"sqli"));
assert!(categories.contains(&"xss"));
assert!(categories.contains(&"path_traversal"));
assert!(categories.contains(&"rce"));
assert!(categories.contains(&"scanner"));
}
#[test]
fn test_high_risk_actor_gets_blocked() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(100);
let actor_id = actor_manager.get_or_create_actor(ip, None);
assert!(!actor_manager.is_blocked(&actor_id));
let blocked = actor_manager.block_actor(&actor_id, "High risk score exceeded threshold");
assert!(blocked);
assert!(actor_manager.is_blocked(&actor_id));
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert!(actor.is_blocked);
assert_eq!(
actor.block_reason,
Some("High risk score exceeded threshold".to_string())
);
assert!(actor.blocked_since.is_some());
}
#[test]
fn test_block_decision_enforced_in_subsequent_requests() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(101);
let fingerprint = "blocked_actor_fingerprint";
let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
actor_manager.block_actor(&actor_id, "Malicious activity detected");
let actor_id2 = actor_manager.get_or_create_actor(ip, Some(fingerprint));
assert_eq!(actor_id, actor_id2);
assert!(actor_manager.is_blocked(&actor_id2));
let ip2 = create_test_ip(102);
let actor_id3 = actor_manager.get_or_create_actor(ip2, Some(fingerprint));
assert_eq!(actor_id, actor_id3);
assert!(actor_manager.is_blocked(&actor_id3));
}
#[test]
fn test_unblock_actor() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(103);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.block_actor(&actor_id, "Test block");
assert!(actor_manager.is_blocked(&actor_id));
let unblocked = actor_manager.unblock_actor(&actor_id);
assert!(unblocked);
assert!(!actor_manager.is_blocked(&actor_id));
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert!(!actor.is_blocked);
assert!(actor.block_reason.is_none());
assert!(actor.blocked_since.is_none());
}
#[test]
fn test_list_blocked_actors() {
let actor_manager = create_test_actor_manager();
for i in 0..10 {
let ip = create_test_ip(i);
let actor_id = actor_manager.get_or_create_actor(ip, None);
if i % 2 == 0 {
actor_manager.block_actor(&actor_id, &format!("Blocked actor {}", i));
}
}
let blocked = actor_manager.list_blocked_actors();
assert_eq!(blocked.len(), 5);
for actor in blocked {
assert!(actor.is_blocked);
}
}
#[test]
fn test_blocking_updates_statistics() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(105);
let actor_id = actor_manager.get_or_create_actor(ip, None);
let stats = actor_manager.stats().snapshot();
assert_eq!(stats.blocked_actors, 0);
actor_manager.block_actor(&actor_id, "Test");
let stats = actor_manager.stats().snapshot();
assert_eq!(stats.blocked_actors, 1);
actor_manager.unblock_actor(&actor_id);
let stats = actor_manager.stats().snapshot();
assert_eq!(stats.blocked_actors, 0);
}
#[test]
fn test_session_token_extraction_and_validation() {
let session_manager = create_test_session_manager();
let ip = create_test_ip(100);
let token_hash = "abc123def456";
let decision = session_manager.validate_request(token_hash, ip, None);
assert_eq!(decision, SessionDecision::New);
assert_eq!(session_manager.len(), 1);
let decision = session_manager.validate_request(token_hash, ip, None);
assert_eq!(decision, SessionDecision::Valid);
assert_eq!(session_manager.len(), 1);
}
#[test]
fn test_valid_session_passes() {
let session_manager = create_test_session_manager();
let ip = create_test_ip(101);
let token_hash = "valid_session_hash";
let ja4 = "t13d1516h2_fingerprint";
session_manager.create_session(token_hash, ip, Some(ja4));
let decision = session_manager.validate_request(token_hash, ip, Some(ja4));
assert_eq!(decision, SessionDecision::Valid);
}
#[test]
fn test_ja4_mismatch_triggers_alert() {
let session_manager = create_test_session_manager();
let ip = create_test_ip(102);
let token_hash = "session_for_hijack_test";
let original_ja4 = "t13d1516h2_original_fingerprint";
let new_ja4 = "t13d1516h2_different_fingerprint";
session_manager.create_session(token_hash, ip, Some(original_ja4));
let decision = session_manager.validate_request(token_hash, ip, Some(new_ja4));
match decision {
SessionDecision::Suspicious(alert) => {
assert_eq!(alert.alert_type, HijackType::Ja4Mismatch);
assert_eq!(alert.original_value, original_ja4);
assert_eq!(alert.new_value, new_ja4);
assert!(
alert.confidence >= 0.9,
"JA4 mismatch should have high confidence"
);
}
_ => panic!(
"Expected Suspicious decision for JA4 mismatch, got {:?}",
decision
),
}
}
#[test]
fn test_expired_session_detected() {
let config = SessionConfig {
session_ttl_secs: 0, idle_timeout_secs: 3600,
..SessionConfig::default()
};
let session_manager = Arc::new(SessionManager::new(config));
let ip = create_test_ip(103);
let token_hash = "expiring_session";
session_manager.create_session(token_hash, ip, None);
std::thread::sleep(std::time::Duration::from_millis(10));
let decision = session_manager.validate_request(token_hash, ip, None);
assert_eq!(decision, SessionDecision::Expired);
}
#[test]
fn test_session_request_count_increments() {
let session_manager = create_test_session_manager();
let ip = create_test_ip(104);
let token_hash = "counting_session";
session_manager.validate_request(token_hash, ip, None); session_manager.validate_request(token_hash, ip, None); session_manager.validate_request(token_hash, ip, None); session_manager.validate_request(token_hash, ip, None);
let session = session_manager.get_session(token_hash).unwrap();
assert_eq!(session.request_count, 4);
}
#[test]
fn test_first_ja4_binds_to_session() {
let session_manager = create_test_session_manager();
let ip = create_test_ip(105);
let token_hash = "binding_session";
let ja4 = "t13d1516h2_bound_fingerprint";
session_manager.create_session(token_hash, ip, None);
let session = session_manager.get_session(token_hash).unwrap();
assert!(session.bound_ja4.is_none());
session_manager.validate_request(token_hash, ip, Some(ja4));
let session = session_manager.get_session(token_hash).unwrap();
assert_eq!(session.bound_ja4, Some(ja4.to_string()));
}
#[test]
fn test_session_with_no_ja4_binding_allows_any_fingerprint() {
let config = SessionConfig {
enable_ja4_binding: false,
..SessionConfig::default()
};
let session_manager = Arc::new(SessionManager::new(config));
let ip = create_test_ip(106);
let token_hash = "unbound_session";
session_manager.create_session(token_hash, ip, Some("original_ja4"));
let decision = session_manager.validate_request(token_hash, ip, Some("different_ja4"));
assert_eq!(decision, SessionDecision::Valid);
}
#[test]
fn test_get_actors_returns_real_data() {
let actor_manager = create_test_actor_manager();
for i in 0..5 {
let ip = create_test_ip(i);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.record_rule_match(&actor_id, &format!("rule-{}", i), 10.0, "test");
}
let api_handler = api::ApiHandler::builder()
.actor_manager(Arc::clone(&actor_manager))
.build();
let actors = api_handler.handle_list_actors(10);
assert_eq!(actors.len(), 5);
for actor in &actors {
assert!(!actor.actor_id.is_empty());
assert_eq!(actor.rule_matches.len(), 1);
assert_eq!(actor.risk_score, 10.0);
}
}
#[test]
fn test_get_sessions_returns_real_data() {
let session_manager = create_test_session_manager();
for i in 0..5 {
let ip = create_test_ip(i);
let token_hash = format!("session_token_{}", i);
session_manager.create_session(&token_hash, ip, Some(&format!("ja4_{}", i)));
}
let api_handler = api::ApiHandler::builder()
.session_manager(Arc::clone(&session_manager))
.build();
let sessions = api_handler.handle_list_sessions(10);
assert_eq!(sessions.len(), 5);
for session in &sessions {
assert!(!session.session_id.is_empty());
assert!(session.session_id.starts_with("sess-"));
assert!(session.bound_ja4.is_some());
}
}
#[test]
fn test_get_actor_stats_returns_real_data() {
let actor_manager = create_test_actor_manager();
for i in 0..10 {
let ip = create_test_ip(i);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.record_rule_match(&actor_id, "test-rule", 5.0, "test");
if i % 3 == 0 {
actor_manager.block_actor(&actor_id, "Test block");
}
}
let api_handler = api::ApiHandler::builder()
.actor_manager(Arc::clone(&actor_manager))
.build();
let stats = api_handler.handle_actor_stats();
assert!(stats.is_some());
let stats = stats.unwrap();
assert_eq!(stats.total_actors, 10);
assert_eq!(stats.blocked_actors, 4); assert_eq!(stats.total_created, 10);
assert_eq!(stats.total_rule_matches, 10);
}
#[test]
fn test_get_session_stats_returns_real_data() {
let session_manager = create_test_session_manager();
for i in 0..5 {
let ip = create_test_ip(i);
let token_hash = format!("session_{}", i);
session_manager.create_session(&token_hash, ip, None);
}
let api_handler = api::ApiHandler::builder()
.session_manager(Arc::clone(&session_manager))
.build();
let stats = api_handler.handle_session_stats();
assert!(stats.is_some());
let stats = stats.unwrap();
assert_eq!(stats.total_sessions, 5);
assert_eq!(stats.active_sessions, 5);
assert_eq!(stats.total_created, 5);
}
#[test]
fn test_api_handler_without_managers_returns_empty() {
let api_handler = api::ApiHandler::builder().build();
let actors = api_handler.handle_list_actors(10);
assert!(actors.is_empty());
let sessions = api_handler.handle_list_sessions(10);
assert!(sessions.is_empty());
let actor_stats = api_handler.handle_actor_stats();
assert!(actor_stats.is_none());
let session_stats = api_handler.handle_session_stats();
assert!(session_stats.is_none());
}
#[test]
fn test_list_actors_respects_limit() {
let actor_manager = create_test_actor_manager();
for i in 0..20 {
let ip = create_test_ip(i);
actor_manager.get_or_create_actor(ip, None);
}
let api_handler = api::ApiHandler::builder()
.actor_manager(Arc::clone(&actor_manager))
.build();
let actors = api_handler.handle_list_actors(5);
assert_eq!(actors.len(), 5);
let actors = api_handler.handle_list_actors(100);
assert_eq!(actors.len(), 20);
}
#[test]
fn test_session_bound_to_actor() {
let actor_manager = create_test_actor_manager();
let session_manager = create_test_session_manager();
let ip = create_test_ip(100);
let fingerprint = "combined_test_fingerprint";
let token_hash = "combined_session_token";
let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
session_manager.create_session(token_hash, ip, Some(fingerprint));
session_manager.bind_to_actor(token_hash, &actor_id);
let session = session_manager.get_session(token_hash).unwrap();
assert_eq!(session.actor_id, Some(actor_id.clone()));
let actor_sessions = session_manager.get_actor_sessions(&actor_id);
assert_eq!(actor_sessions.len(), 1);
assert_eq!(actor_sessions[0].token_hash, token_hash);
}
#[test]
fn test_multi_session_single_actor() {
let actor_manager = create_test_actor_manager();
let session_manager = create_test_session_manager();
let ip = create_test_ip(101);
let fingerprint = "multi_session_fingerprint";
let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
session_manager.create_session("session_tab_1", ip, Some(fingerprint));
session_manager.create_session("session_tab_2", ip, Some(fingerprint));
session_manager.create_session("session_mobile", ip, Some(fingerprint));
session_manager.bind_to_actor("session_tab_1", &actor_id);
session_manager.bind_to_actor("session_tab_2", &actor_id);
session_manager.bind_to_actor("session_mobile", &actor_id);
let actor_sessions = session_manager.get_actor_sessions(&actor_id);
assert_eq!(actor_sessions.len(), 3);
}
#[test]
fn test_blocked_actor_affects_all_sessions() {
let actor_manager = create_test_actor_manager();
let session_manager = create_test_session_manager();
let ip = create_test_ip(102);
let fingerprint = "blocked_user_fingerprint";
let actor_id = actor_manager.get_or_create_actor(ip, Some(fingerprint));
session_manager.create_session("blocked_user_session_1", ip, Some(fingerprint));
session_manager.create_session("blocked_user_session_2", ip, Some(fingerprint));
session_manager.bind_to_actor("blocked_user_session_1", &actor_id);
session_manager.bind_to_actor("blocked_user_session_2", &actor_id);
actor_manager.block_actor(&actor_id, "Malicious activity");
assert!(actor_manager.is_blocked(&actor_id));
let sessions = session_manager.get_actor_sessions(&actor_id);
assert_eq!(sessions.len(), 2);
let actor_id_check = actor_manager.get_or_create_actor(ip, Some(fingerprint));
assert_eq!(actor_id, actor_id_check);
assert!(actor_manager.is_blocked(&actor_id_check));
}
#[test]
fn test_risk_accumulation_workflow() {
let actor_manager = create_test_actor_manager();
let ip = create_test_ip(103);
let actor_id = actor_manager.get_or_create_actor(ip, None);
actor_manager.record_rule_match(&actor_id, "sqli-001", 30.0, "sqli");
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert_eq!(actor.risk_score, 30.0);
assert!(!actor_manager.is_blocked(&actor_id));
actor_manager.record_rule_match(&actor_id, "sqli-002", 40.0, "sqli");
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert_eq!(actor.risk_score, 70.0);
assert!(!actor_manager.is_blocked(&actor_id));
actor_manager.record_rule_match(&actor_id, "xss-001", 20.0, "xss");
let actor = actor_manager.get_actor(&actor_id).unwrap();
assert_eq!(actor.risk_score, 90.0);
if actor.risk_score >= 80.0 {
actor_manager.block_actor(&actor_id, "Risk threshold exceeded");
}
assert!(actor_manager.is_blocked(&actor_id));
}
}