systemprompt_analytics/services/behavioral_detector/
mod.rs1mod checks;
6mod fingerprint_checks;
7mod helpers;
8mod types;
9
10pub use types::{BehavioralAnalysisInput, BehavioralAnalysisResult, BehavioralSignal, SignalType};
11
12pub const BEHAVIORAL_BOT_THRESHOLD: i32 = 30;
13
14pub mod scoring {
15 pub const HIGH_REQUEST_COUNT: i32 = 30;
16 pub const HIGH_PAGE_COVERAGE: i32 = 25;
17 pub const SEQUENTIAL_NAVIGATION: i32 = 20;
18 pub const MULTIPLE_FINGERPRINT_SESSIONS: i32 = 20;
19 pub const REGULAR_TIMING: i32 = 15;
20 pub const HIGH_PAGES_PER_MINUTE: i32 = 15;
21 pub const OUTDATED_BROWSER: i32 = 25;
22 pub const NO_JAVASCRIPT_EVENTS: i32 = 20;
23 pub const GHOST_SESSION: i32 = 35;
24 pub const RESIDENTIAL_PROXY_ROTATION: i32 = 35;
25 pub const NO_ENGAGEMENT_ACROSS_SESSIONS: i32 = 25;
26 pub const PERIODIC_CADENCE: i32 = 35;
27 pub const HOME_TAB_WATCHER: i32 = 35;
28}
29
30pub mod thresholds {
31 pub const REQUEST_COUNT_LIMIT: i64 = 50;
32 pub const PAGE_COVERAGE_PERCENT: f64 = 60.0;
33 pub const FINGERPRINT_SESSION_LIMIT: i64 = 5;
34 pub const PAGES_PER_MINUTE_LIMIT: f64 = 5.0;
35 pub const TIMING_VARIANCE_MIN: f64 = 0.1;
36 pub const CHROME_MIN_VERSION: i32 = 120;
37 pub const FIREFOX_MIN_VERSION: i32 = 120;
38 pub const NO_JS_MIN_REQUESTS: i64 = 2;
39 pub const GHOST_SESSION_MIN_AGE_SECONDS: i64 = 30;
40 pub const RESIDENTIAL_PROXY_IP_RATIO: f64 = 0.8;
41 pub const RESIDENTIAL_PROXY_MIN_SESSIONS: i64 = 5;
42 pub const NO_ENGAGEMENT_MIN_SESSIONS: i64 = 10;
43 pub const PERIODIC_CADENCE_MIN_SESSIONS: usize = 5;
44 pub const PERIODIC_CADENCE_MAX_CV: f64 = 0.1;
45 pub const HOME_TAB_REQUEST_CEILING: i64 = 2;
46 pub const HOME_TAB_DAILY_GAP_SECONDS_MIN: f64 = 60.0 * 60.0 * 20.0;
47 pub const HOME_TAB_DAILY_GAP_SECONDS_MAX: f64 = 60.0 * 60.0 * 28.0;
48}
49
50#[derive(Debug, Clone, Copy, Default)]
51pub struct BehavioralBotDetector;
52
53impl BehavioralBotDetector {
54 pub const fn new() -> Self {
55 Self
56 }
57
58 pub fn analyze(input: &BehavioralAnalysisInput) -> BehavioralAnalysisResult {
59 let mut signals = Vec::new();
60 let mut score = 0;
61
62 Self::check_high_request_count(input, &mut score, &mut signals);
63 Self::check_high_page_coverage(input, &mut score, &mut signals);
64 Self::check_sequential_navigation(input, &mut score, &mut signals);
65 Self::check_multiple_fingerprint_sessions(input, &mut score, &mut signals);
66 Self::check_regular_timing(input, &mut score, &mut signals);
67 Self::check_high_pages_per_minute(input, &mut score, &mut signals);
68 Self::check_outdated_browser(input, &mut score, &mut signals);
69 Self::check_no_javascript_events(input, &mut score, &mut signals);
70 Self::check_ghost_session(input, &mut score, &mut signals);
71 Self::check_residential_proxy_rotation(input, &mut score, &mut signals);
72 Self::check_no_engagement_across_sessions(input, &mut score, &mut signals);
73 Self::check_periodic_cadence(input, &mut score, &mut signals);
74 Self::check_home_tab_watcher(input, &mut score, &mut signals);
75
76 let is_suspicious = score >= BEHAVIORAL_BOT_THRESHOLD;
77 let reason = is_suspicious.then(|| {
78 signals
79 .iter()
80 .map(|s| s.signal_type.to_string())
81 .collect::<Vec<_>>()
82 .join(", ")
83 });
84
85 BehavioralAnalysisResult {
86 score,
87 is_suspicious,
88 signals,
89 reason,
90 }
91 }
92}