use crate::forensics::ForensicConfig;
use crate::types::{Cookie, Error, Result};
use chrono::{DateTime, Duration, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::net::IpAddr;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CookieTimeline {
pub events: Vec<CookieEvent>,
pub reconstruction_confidence: f64,
pub anomalies: Vec<TimelineAnomaly>,
pub start_time: DateTime<Utc>,
pub end_time: DateTime<Utc>,
pub duration: Duration,
pub statistics: TimelineStatistics,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CookieEvent {
pub timestamp: DateTime<Utc>,
pub cookie: Cookie,
pub action: CookieAction,
pub source: EventSource,
pub ip_address: Option<IpAddr>,
pub user_agent: Option<String>,
pub confidence: f64,
pub metadata: HashMap<String, String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum CookieAction {
Created,
Modified,
Accessed,
Deleted,
Expired,
AttributeChanged,
Unknown,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum EventSource {
ServerSetCookie,
JavaScript,
Browser,
ThirdParty {
domain: String,
},
Unknown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineAnomaly {
pub anomaly_type: AnomalyType,
pub severity: f64,
pub description: String,
pub time_range: (DateTime<Utc>, DateTime<Utc>),
pub related_events: Vec<usize>,
pub confidence: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum AnomalyType {
SuspiciousGap,
ActivityBurst,
ModificationWithoutAccess,
ImpossibleTravel,
SessionHijacking,
ReplayAttack,
TemporalInconsistency,
EvidenceTampering,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TimelineStatistics {
pub total_events: usize,
pub events_by_action: HashMap<String, usize>,
pub events_by_source: HashMap<String, usize>,
pub unique_cookies: usize,
pub avg_events_per_day: f64,
pub peak_activity: Option<DateTime<Utc>>,
pub longest_gap: Option<Duration>,
}
impl CookieTimeline {
pub fn reconstruct(cookies: &[Cookie], config: &ForensicConfig) -> Result<Self> {
if cookies.is_empty() {
return Err(Error::ForensicError(
"No cookies provided for timeline reconstruction".to_string(),
));
}
let mut events = Self::cookies_to_events(cookies);
events.sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
let start_time = events.first().map_or_else(Utc::now, |e| e.timestamp);
let end_time = events.last().map_or_else(Utc::now, |e| e.timestamp);
let duration = end_time - start_time;
let statistics = Self::calculate_statistics(&events, duration);
let reconstruction_confidence = Self::calculate_confidence(&events, cookies);
let mut timeline = Self {
events,
reconstruction_confidence,
anomalies: Vec::new(),
start_time,
end_time,
duration,
statistics,
};
timeline.anomalies = timeline.detect_anomalies(config);
Ok(timeline)
}
fn cookies_to_events(cookies: &[Cookie]) -> Vec<CookieEvent> {
let mut events = Vec::new();
for cookie in cookies {
events.push(CookieEvent {
timestamp: cookie.created_at,
cookie: cookie.clone(),
action: CookieAction::Created,
source: Self::detect_source(cookie),
ip_address: None,
user_agent: None,
confidence: 0.9,
metadata: HashMap::new(),
});
if let Some(expires) = cookie.expires {
if expires > Utc::now() {
events.push(CookieEvent {
timestamp: expires,
cookie: cookie.clone(),
action: CookieAction::Expired,
source: EventSource::Browser,
ip_address: None,
user_agent: None,
confidence: 1.0,
metadata: HashMap::new(),
});
}
}
}
events
}
fn detect_source(cookie: &Cookie) -> EventSource {
if let Some(ref domain) = cookie.domain {
if domain.starts_with('.') {
return EventSource::ThirdParty {
domain: domain.clone(),
};
}
}
if cookie.http_only {
EventSource::ServerSetCookie
} else {
EventSource::JavaScript
}
}
#[allow(clippy::cast_precision_loss)] fn calculate_statistics(events: &[CookieEvent], duration: Duration) -> TimelineStatistics {
let mut events_by_action: HashMap<String, usize> = HashMap::new();
let mut events_by_source: HashMap<String, usize> = HashMap::new();
let mut unique_cookies = std::collections::HashSet::new();
for event in events {
let action_str = format!("{:?}", event.action);
*events_by_action.entry(action_str).or_insert(0) += 1;
let source_str = match &event.source {
EventSource::ServerSetCookie => "Server",
EventSource::JavaScript => "JavaScript",
EventSource::Browser => "Browser",
EventSource::ThirdParty { .. } => "ThirdParty",
EventSource::Unknown => "Unknown",
};
*events_by_source.entry(source_str.to_string()).or_insert(0) += 1;
unique_cookies.insert(event.cookie.name.clone());
}
let avg_events_per_day = if duration.num_days() > 0 {
events.len() as f64 / duration.num_days() as f64
} else {
events.len() as f64
};
TimelineStatistics {
total_events: events.len(),
events_by_action,
events_by_source,
unique_cookies: unique_cookies.len(),
avg_events_per_day,
peak_activity: None, longest_gap: None, }
}
#[allow(clippy::cast_precision_loss)] fn calculate_confidence(events: &[CookieEvent], cookies: &[Cookie]) -> f64 {
if events.is_empty() || cookies.is_empty() {
return 0.0;
}
let mut confidence = 0.5;
confidence += 0.3;
let has_expiry_times =
cookies.iter().filter(|c| c.expires.is_some()).count() as f64 / cookies.len() as f64;
confidence += has_expiry_times * 0.2;
confidence.min(1.0)
}
fn detect_anomalies(&self, config: &ForensicConfig) -> Vec<TimelineAnomaly> {
let mut anomalies = Vec::new();
anomalies.extend(self.detect_suspicious_gaps(config));
anomalies.extend(self.detect_activity_bursts(config));
anomalies.extend(Self::detect_impossible_travel(config));
anomalies.retain(|a| a.confidence >= config.anomaly_threshold);
anomalies
}
#[allow(clippy::cast_precision_loss)] fn detect_suspicious_gaps(&self, _config: &ForensicConfig) -> Vec<TimelineAnomaly> {
let mut anomalies = Vec::new();
for window in self.events.windows(2) {
let gap = window[1].timestamp - window[0].timestamp;
if gap > Duration::hours(1) {
let severity = (gap.num_hours() as f64 / 24.0).min(1.0);
anomalies.push(TimelineAnomaly {
anomaly_type: AnomalyType::SuspiciousGap,
severity,
description: format!("Suspicious gap of {} hours in timeline", gap.num_hours()),
time_range: (window[0].timestamp, window[1].timestamp),
related_events: vec![0, 1], confidence: 0.7,
});
}
}
anomalies
}
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_sign_loss)] #[allow(clippy::cast_possible_wrap)] fn detect_activity_bursts(&self, _config: &ForensicConfig) -> Vec<TimelineAnomaly> {
let mut anomalies = Vec::new();
let mut windows: HashMap<i64, Vec<&CookieEvent>> = HashMap::new();
for event in &self.events {
let window = event.timestamp.timestamp() / 60; windows.entry(window).or_default().push(event);
}
let avg_events_per_window = self.events.len() as f64 / windows.len().max(1) as f64;
for (window, events) in windows {
if events.len() as f64 > avg_events_per_window * 3.0 {
let timestamp = DateTime::from_timestamp(window * 60, 0).unwrap_or_else(Utc::now);
anomalies.push(TimelineAnomaly {
anomaly_type: AnomalyType::ActivityBurst,
severity: 0.6,
description: format!("Unusual burst of {} events in 1 minute", events.len()),
time_range: (timestamp, timestamp + Duration::minutes(1)),
related_events: Vec::new(),
confidence: 0.8,
});
}
}
anomalies
}
fn detect_impossible_travel(_config: &ForensicConfig) -> Vec<TimelineAnomaly> {
Vec::new()
}
#[must_use]
pub fn find_anomalies(&self) -> Vec<&TimelineAnomaly> {
self.anomalies.iter().collect()
}
#[must_use]
pub fn generate_forensic_report(&self) -> crate::forensics::ForensicReport {
crate::forensics::report_generator::ForensicReportBuilder::new()
.with_timeline(self.clone())
.build()
.expect("Failed to generate forensic report")
}
}
#[cfg(test)]
mod tests {
#[test]
fn test_timeline_creation() {
}
#[test]
fn test_anomaly_detection() {
}
}