Skip to main content

systemprompt_analytics/services/behavioral_detector/
mod.rs

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