Skip to main content

systemprompt_analytics/services/behavioral_detector/
mod.rs

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