use crate::types::{Cookie, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug)]
pub struct BehavioralAnalyzer {
baseline: Option<BehavioralBaseline>,
}
impl BehavioralAnalyzer {
#[must_use]
pub fn new() -> Self {
Self { baseline: None }
}
pub fn train(&mut self, normal_cookies: &[Cookie]) -> Result<()> {
let mut baseline = BehavioralBaseline::new();
baseline.learn(normal_cookies);
self.baseline = Some(baseline);
tracing::info!(
"Learned behavioral baseline from {} cookies",
normal_cookies.len()
);
Ok(())
}
pub fn detect_changes(&self, cookies: &[Cookie]) -> Result<Vec<BehaviorChange>> {
let baseline = self.baseline.as_ref().ok_or_else(|| {
crate::types::Error::InvalidState("Behavioral analyzer not trained".to_string())
})?;
let mut changes = Vec::new();
for cookie in cookies {
if let Some(change) = baseline.detect_deviation(cookie) {
changes.push(change);
}
}
Ok(changes)
}
pub fn analyze(&self, cookies: &[Cookie]) -> Result<BehaviorReport> {
let baseline = self.baseline.as_ref().ok_or_else(|| {
crate::types::Error::InvalidState("Behavioral analyzer not trained".to_string())
})?;
Ok(BehaviorReport {
baseline: baseline.clone(),
cookies_analyzed: cookies.len(),
deviations: self.detect_changes(cookies)?,
timestamp: Utc::now(),
})
}
}
impl Default for BehavioralAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehavioralBaseline {
pub normal_cookie_count: (usize, usize),
pub common_names: Vec<String>,
pub common_domains: Vec<String>,
pub security_patterns: SecurityPatterns,
}
impl BehavioralBaseline {
fn new() -> Self {
Self {
normal_cookie_count: (0, 0),
common_names: Vec::new(),
common_domains: Vec::new(),
security_patterns: SecurityPatterns::default(),
}
}
#[allow(clippy::cast_precision_loss)] fn learn(&mut self, cookies: &[Cookie]) {
self.normal_cookie_count = (cookies.len().saturating_sub(10), cookies.len() + 10);
let mut name_freq: HashMap<String, usize> = HashMap::new();
for cookie in cookies {
*name_freq.entry(cookie.name.clone()).or_insert(0) += 1;
}
self.common_names = name_freq
.into_iter()
.filter(|(_, count)| *count > cookies.len() / 10)
.map(|(name, _)| name)
.collect();
let mut domain_freq: HashMap<String, usize> = HashMap::new();
for cookie in cookies {
if let Some(ref domain) = cookie.domain {
*domain_freq.entry(domain.clone()).or_insert(0) += 1;
}
}
self.common_domains = domain_freq
.into_iter()
.filter(|(_, count)| *count > cookies.len() / 10)
.map(|(domain, _)| domain)
.collect();
let secure_count = cookies.iter().filter(|c| c.secure).count();
let httponly_count = cookies.iter().filter(|c| c.http_only).count();
self.security_patterns.typical_secure_percentage =
secure_count as f64 / cookies.len() as f64;
self.security_patterns.typical_httponly_percentage =
httponly_count as f64 / cookies.len() as f64;
}
fn detect_deviation(&self, cookie: &Cookie) -> Option<BehaviorChange> {
let mut reasons = Vec::new();
if !self.common_names.contains(&cookie.name) {
reasons.push("Uncommon cookie name".to_string());
}
if let Some(ref domain) = cookie.domain {
if !self.common_domains.contains(domain) {
reasons.push("Uncommon domain".to_string());
}
}
if !cookie.secure && self.security_patterns.typical_secure_percentage > 0.7 {
reasons.push("Missing Secure flag (uncommon for this site)".to_string());
}
if reasons.is_empty() {
None
} else {
Some(BehaviorChange {
cookie: cookie.clone(),
reasons,
severity_score: 0.6,
description: "Cookie behavior deviates from learned baseline".to_string(),
})
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SecurityPatterns {
pub typical_secure_percentage: f64,
pub typical_httponly_percentage: f64,
}
impl Default for SecurityPatterns {
fn default() -> Self {
Self {
typical_secure_percentage: 0.5,
typical_httponly_percentage: 0.5,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorChange {
pub cookie: Cookie,
pub reasons: Vec<String>,
pub severity_score: f64,
pub description: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BehaviorReport {
pub baseline: BehavioralBaseline,
pub cookies_analyzed: usize,
pub deviations: Vec<BehaviorChange>,
pub timestamp: DateTime<Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_behavioral_analyzer_creation() {
let analyzer = BehavioralAnalyzer::new();
assert!(analyzer.baseline.is_none());
}
}