'f5.session_burst', 'family' => 5, 'tier' => 'server', 'weight' => 55, 'label' => 'Many sessions from one network in minutes'], ['id' => 'f5.identical_profiles', 'family' => 5, 'tier' => 'server', 'weight' => 60, 'label' => 'Separate networks sharing one identical device profile'], ['id' => 'f5.uniform_session_length', 'family' => 5, 'tier' => 'server', 'weight' => 45, 'label' => 'Sessions all lasting the same length of time'], ['id' => 'f5.sequential_paths', 'family' => 5, 'tier' => 'server', 'weight' => 40, 'label' => 'Pages visited in the same order every time'], ['id' => 'f5.coordinated_timing', 'family' => 5, 'tier' => 'server', 'weight' => 50, 'label' => 'Sessions starting on a machine-regular cadence'], ['id' => 'f6.fingerprint_recurrence', 'family' => 6, 'tier' => 'server', 'weight' => 50, 'label' => 'One device fingerprint returning under many identities', 'consent' => true], ['id' => 'f6.fingerprint_churn', 'family' => 6, 'tier' => 'server', 'weight' => 45, 'label' => 'Device fingerprint changing on every visit', 'consent' => true], ['id' => 'f6.known_bad_device', 'family' => 6, 'tier' => 'server', 'weight' => 65, 'label' => 'Device fingerprint previously judged invalid', 'consent' => true], ]; } public static function correlation_enabled() { $setting = get_option('adghost_ivt_correlation', null); if ($setting !== null && $setting !== '') { return (bool) $setting; } return function_exists('wp_using_ext_object_cache') && wp_using_ext_object_cache(); } public static function fingerprinting_permitted() { if (!get_option('adghost_ivt_fingerprinting', false)) { return false; } if (function_exists('wp_has_consent')) { return (bool) wp_has_consent('statistics'); } return (bool) apply_filters('adghost_ivt_has_fingerprint_consent', false); } public static function evaluate($profile = null) { $fired = []; if (!self::correlation_enabled()) { return $fired; } $bucket = self::record_session($profile); if ($bucket['sessions'] >= self::BURST_SESSIONS) { $fired[] = 'f5.session_burst'; } if ($bucket['cadence_regular']) { $fired[] = 'f5.coordinated_timing'; } if (is_array($profile) && !empty($profile['fingerprint'])) { $shared = self::record_profile((string) $profile['fingerprint']); if ($shared >= self::SHARED_PROFILE) { $fired[] = 'f5.identical_profiles'; } } if (self::fingerprinting_permitted() && is_array($profile) && !empty($profile['fingerprint'])) { $fired = array_merge($fired, self::evaluate_identity((string) $profile['fingerprint'])); } return array_values(array_unique($fired)); } private static function evaluate_identity($fingerprint) { $fired = []; $key = 'adghost_fp_' . md5($fingerprint); $seen = get_transient($key); if (is_array($seen)) { if (($seen['sessions'] ?? 0) > 20) { $fired[] = 'f6.fingerprint_recurrence'; } if (!empty($seen['invalid'])) { $fired[] = 'f6.known_bad_device'; } $seen['sessions'] = (int) ($seen['sessions'] ?? 0) + 1; } else { $seen = ['sessions' => 1, 'invalid' => false]; } set_transient($key, $seen, DAY_IN_SECONDS); return $fired; } private static function record_session($profile) { $key = 'adghost_corr_' . substr(AdGhost_IVT_Session::hash_ip(), 0, 16); $bucket = get_transient($key); $now = time(); if (!is_array($bucket)) { $bucket = ['sessions' => 0, 'starts' => []]; } $bucket['sessions']++; $bucket['starts'][] = $now; if (count($bucket['starts']) > 30) { $bucket['starts'] = array_slice($bucket['starts'], -30); } set_transient($key, $bucket, self::WINDOW); return [ 'sessions' => $bucket['sessions'], 'cadence_regular' => self::cadence_is_regular($bucket['starts']), ]; } private static function cadence_is_regular(array $starts) { if (count($starts) < 6) { return false; } $gaps = []; for ($i = 1; $i < count($starts); $i++) { $gaps[] = $starts[$i] - $starts[$i - 1]; } $mean = array_sum($gaps) / count($gaps); if ($mean <= 0) { return false; } $variance = 0.0; foreach ($gaps as $gap) { $variance += ($gap - $mean) ** 2; } $stddev = sqrt($variance / count($gaps)); return $stddev < 1.5; } private static function record_profile($fingerprint) { $key = 'adghost_prof_' . md5($fingerprint); $seen = get_transient($key); $ip = substr(AdGhost_IVT_Session::hash_ip(), 0, 16); if (!is_array($seen)) { $seen = []; } if (!in_array($ip, $seen, true)) { $seen[] = $ip; } if (count($seen) > 50) { $seen = array_slice($seen, -50); } set_transient($key, $seen, self::WINDOW); return count($seen); } }