use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use lazy_static::lazy_static;
use libm;
use spin::Mutex;
const ANOMALY_RAPID_CREATE_THRESHOLD: usize = 100;
const ANOMALY_MASS_DELETE_THRESHOLD: usize = 50;
const ANOMALY_TIME_WINDOW_SECONDS: u64 = 3600;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnomalyType {
RapidCreation,
MassDeletion,
UnusualTime,
ExtensionChange,
HighEntropy,
UnusualLocation,
MassEncryption,
}
impl AnomalyType {
pub fn severity(&self) -> u32 {
match self {
AnomalyType::MassEncryption => 95, AnomalyType::RapidCreation => 80,
AnomalyType::ExtensionChange => 85,
AnomalyType::HighEntropy => 70,
AnomalyType::MassDeletion => 90,
AnomalyType::UnusualTime => 40,
AnomalyType::UnusualLocation => 50,
}
}
pub fn name(&self) -> &'static str {
match self {
AnomalyType::RapidCreation => "rapid_creation",
AnomalyType::MassDeletion => "mass_deletion",
AnomalyType::UnusualTime => "unusual_time",
AnomalyType::ExtensionChange => "extension_change",
AnomalyType::HighEntropy => "high_entropy",
AnomalyType::UnusualLocation => "unusual_location",
AnomalyType::MassEncryption => "mass_encryption",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnomalyAction {
Allow,
Warn,
Block,
Quarantine,
}
impl AnomalyAction {
pub fn from_severity(severity: u32) -> Self {
match severity {
0..=40 => AnomalyAction::Allow,
41..=70 => AnomalyAction::Warn,
71..=89 => AnomalyAction::Block,
90..=100 => AnomalyAction::Quarantine,
_ => AnomalyAction::Block,
}
}
pub fn should_block(&self) -> bool {
matches!(self, AnomalyAction::Block | AnomalyAction::Quarantine)
}
}
#[derive(Debug, Clone)]
pub struct Anomaly {
pub id: u64,
pub anomaly_type: AnomalyType,
pub timestamp: u64,
pub user_id: u64,
pub dataset_id: u64,
pub severity: u32,
pub details: &'static str,
pub action_taken: bool,
pub action: AnomalyAction,
}
#[derive(Debug, Clone)]
pub struct BehaviorBaseline {
pub user_id: u64,
pub avg_creates_per_hour: f64,
pub var_creates_per_hour: f64,
pub avg_deletes_per_hour: f64,
pub var_deletes_per_hour: f64,
pub active_hours: u32,
sample_count: u64,
}
impl BehaviorBaseline {
pub fn new(user_id: u64) -> Self {
Self {
user_id,
avg_creates_per_hour: 0.0,
var_creates_per_hour: 0.0,
avg_deletes_per_hour: 0.0,
var_deletes_per_hour: 0.0,
active_hours: 0,
sample_count: 0,
}
}
pub fn update_creates(&mut self, creates: f64) {
self.sample_count += 1;
let n = self.sample_count as f64;
let delta = creates - self.avg_creates_per_hour;
self.avg_creates_per_hour += delta / n;
let delta2 = creates - self.avg_creates_per_hour;
self.var_creates_per_hour += delta * delta2;
}
pub fn update_deletes(&mut self, deletes: f64) {
let n = (self.sample_count + 1) as f64;
let delta = deletes - self.avg_deletes_per_hour;
self.avg_deletes_per_hour += delta / n;
let delta2 = deletes - self.avg_deletes_per_hour;
self.var_deletes_per_hour += delta * delta2;
}
pub fn mark_active_hour(&mut self, hour: u32) {
if hour < 24 {
self.active_hours |= 1 << hour;
}
}
pub fn is_typical_hour(&self, hour: u32) -> bool {
if hour >= 24 {
return false;
}
(self.active_hours & (1 << hour)) != 0
}
pub fn creates_stddev(&self) -> f64 {
if self.sample_count < 2 {
return 0.0;
}
libm::sqrt(self.var_creates_per_hour / (self.sample_count - 1) as f64)
}
pub fn is_creates_anomalous(&self, count: f64) -> bool {
if self.sample_count < 10 {
return false; }
let sigma = self.creates_stddev();
let z_score = (count - self.avg_creates_per_hour).abs() / sigma.max(1.0);
z_score > 3.0 }
pub fn is_deletes_anomalous(&self, count: f64) -> bool {
if self.sample_count < 10 {
return false;
}
let sigma = libm::sqrt(self.var_deletes_per_hour / (self.sample_count - 1) as f64);
let z_score = (count - self.avg_deletes_per_hour).abs() / sigma.max(1.0);
z_score > 3.0
}
}
#[derive(Debug, Clone, Default)]
pub struct AnomalyStats {
pub total_anomalies: u64,
pub by_type: BTreeMap<&'static str, u64>,
pub actions_taken: u64,
pub false_positives: u64,
}
lazy_static! {
static ref ANOMALY_DETECTOR: Mutex<AnomalyDetector> = Mutex::new(AnomalyDetector::new());
}
pub struct AnomalyDetector {
baselines: BTreeMap<u64, BehaviorBaseline>,
recent_creates: BTreeMap<u64, Vec<u64>>, recent_deletes: BTreeMap<u64, Vec<u64>>,
anomalies: Vec<Anomaly>,
next_anomaly_id: u64,
stats: AnomalyStats,
}
impl Default for AnomalyDetector {
fn default() -> Self {
Self::new()
}
}
impl AnomalyDetector {
pub fn new() -> Self {
Self {
baselines: BTreeMap::new(),
recent_creates: BTreeMap::new(),
recent_deletes: BTreeMap::new(),
anomalies: Vec::new(),
next_anomaly_id: 1,
stats: AnomalyStats::default(),
}
}
pub fn record_create(
&mut self,
user_id: u64,
dataset_id: u64,
timestamp: u64,
) -> Option<Anomaly> {
let creates = self.recent_creates.entry(user_id).or_default();
creates.push(timestamp);
creates.retain(|&t| timestamp.saturating_sub(t) < ANOMALY_TIME_WINDOW_SECONDS);
if creates.len() > ANOMALY_RAPID_CREATE_THRESHOLD {
return self.detect_anomaly(
AnomalyType::RapidCreation,
user_id,
dataset_id,
timestamp,
"Rapid file creation detected",
);
}
let baseline = self
.baselines
.entry(user_id)
.or_insert_with(|| BehaviorBaseline::new(user_id));
if baseline.is_creates_anomalous(creates.len() as f64) {
return self.detect_anomaly(
AnomalyType::RapidCreation,
user_id,
dataset_id,
timestamp,
"Unusual creation rate",
);
}
baseline.update_creates(creates.len() as f64);
None
}
pub fn record_delete(
&mut self,
user_id: u64,
dataset_id: u64,
timestamp: u64,
) -> Option<Anomaly> {
let deletes = self.recent_deletes.entry(user_id).or_default();
deletes.push(timestamp);
deletes.retain(|&t| timestamp.saturating_sub(t) < ANOMALY_TIME_WINDOW_SECONDS);
if deletes.len() > ANOMALY_MASS_DELETE_THRESHOLD {
return self.detect_anomaly(
AnomalyType::MassDeletion,
user_id,
dataset_id,
timestamp,
"Mass deletion detected",
);
}
None
}
pub fn check_access_time(
&mut self,
user_id: u64,
dataset_id: u64,
timestamp: u64,
) -> Option<Anomaly> {
let hour = ((timestamp / 3600) % 24) as u32;
let baseline = self
.baselines
.entry(user_id)
.or_insert_with(|| BehaviorBaseline::new(user_id));
if !baseline.is_typical_hour(hour) && baseline.sample_count > 50 {
return self.detect_anomaly(
AnomalyType::UnusualTime,
user_id,
dataset_id,
timestamp,
"Access during unusual hours",
);
}
baseline.mark_active_hour(hour);
None
}
pub fn check_entropy(
&mut self,
user_id: u64,
dataset_id: u64,
timestamp: u64,
entropy: f64,
) -> Option<Anomaly> {
if entropy > 7.5 {
return self.detect_anomaly(
AnomalyType::HighEntropy,
user_id,
dataset_id,
timestamp,
"High entropy file (potential encryption)",
);
}
None
}
pub fn detect_ransomware(
&mut self,
user_id: u64,
dataset_id: u64,
timestamp: u64,
) -> Option<Anomaly> {
let creates = self
.recent_creates
.get(&user_id)
.map(|v| v.len())
.unwrap_or(0);
let deletes = self
.recent_deletes
.get(&user_id)
.map(|v| v.len())
.unwrap_or(0);
if creates > 50 && deletes > 50 {
return self.detect_anomaly(
AnomalyType::MassEncryption,
user_id,
dataset_id,
timestamp,
"Ransomware pattern detected",
);
}
None
}
fn detect_anomaly(
&mut self,
anomaly_type: AnomalyType,
user_id: u64,
dataset_id: u64,
timestamp: u64,
details: &'static str,
) -> Option<Anomaly> {
let severity = anomaly_type.severity();
let action = AnomalyAction::from_severity(severity);
let anomaly = Anomaly {
id: self.next_anomaly_id,
anomaly_type,
timestamp,
user_id,
dataset_id,
severity,
details,
action_taken: action.should_block(),
action,
};
self.next_anomaly_id += 1;
self.anomalies.push(anomaly.clone());
self.stats.total_anomalies += 1;
*self.stats.by_type.entry(anomaly_type.name()).or_insert(0) += 1;
if action.should_block() {
self.stats.actions_taken += 1;
}
Some(anomaly)
}
pub fn get_recent_anomalies(&self, limit: usize) -> Vec<Anomaly> {
self.anomalies.iter().rev().take(limit).cloned().collect()
}
pub fn get_user_anomalies(&self, user_id: u64) -> Vec<Anomaly> {
self.anomalies
.iter()
.filter(|a| a.user_id == user_id)
.cloned()
.collect()
}
pub fn get_stats(&self) -> AnomalyStats {
self.stats.clone()
}
}
pub struct AnomalyEngine;
impl AnomalyEngine {
pub fn check_create(user_id: u64, dataset_id: u64, timestamp: u64) -> Result<(), &'static str> {
let mut detector = ANOMALY_DETECTOR.lock();
if let Some(anomaly) = detector.record_create(user_id, dataset_id, timestamp) {
if anomaly.action.should_block() {
return Err(anomaly.details);
}
}
Ok(())
}
pub fn check_delete(user_id: u64, dataset_id: u64, timestamp: u64) -> Result<(), &'static str> {
let mut detector = ANOMALY_DETECTOR.lock();
if let Some(anomaly) = detector.record_delete(user_id, dataset_id, timestamp) {
if anomaly.action.should_block() {
return Err(anomaly.details);
}
}
Ok(())
}
pub fn check_write(
user_id: u64,
dataset_id: u64,
timestamp: u64,
data: &[u8],
) -> Result<(), &'static str> {
let entropy = Self::calculate_entropy(data);
let mut detector = ANOMALY_DETECTOR.lock();
if let Some(anomaly) = detector.check_entropy(user_id, dataset_id, timestamp, entropy) {
if anomaly.action.should_block() {
return Err(anomaly.details);
}
}
if let Some(anomaly) = detector.detect_ransomware(user_id, dataset_id, timestamp) {
if anomaly.action.should_block() {
return Err(anomaly.details);
}
}
Ok(())
}
fn calculate_entropy(data: &[u8]) -> f64 {
if data.is_empty() {
return 0.0;
}
let mut counts = [0u64; 256];
for &byte in data {
counts[byte as usize] += 1;
}
let len = data.len() as f64;
let mut entropy = 0.0;
for &count in &counts {
if count > 0 {
let p = count as f64 / len;
entropy -= p * libm::log2(p);
}
}
entropy
}
pub fn record_create(user_id: u64, dataset_id: u64, timestamp: u64) -> Option<Anomaly> {
let mut detector = ANOMALY_DETECTOR.lock();
detector.record_create(user_id, dataset_id, timestamp)
}
pub fn record_delete(user_id: u64, dataset_id: u64, timestamp: u64) -> Option<Anomaly> {
let mut detector = ANOMALY_DETECTOR.lock();
detector.record_delete(user_id, dataset_id, timestamp)
}
pub fn check_access_time(user_id: u64, dataset_id: u64, timestamp: u64) -> Option<Anomaly> {
let mut detector = ANOMALY_DETECTOR.lock();
detector.check_access_time(user_id, dataset_id, timestamp)
}
pub fn check_entropy(
user_id: u64,
dataset_id: u64,
timestamp: u64,
entropy: f64,
) -> Option<Anomaly> {
let mut detector = ANOMALY_DETECTOR.lock();
detector.check_entropy(user_id, dataset_id, timestamp, entropy)
}
pub fn detect_ransomware(user_id: u64, dataset_id: u64, timestamp: u64) -> Option<Anomaly> {
let mut detector = ANOMALY_DETECTOR.lock();
detector.detect_ransomware(user_id, dataset_id, timestamp)
}
pub fn recent_anomalies(limit: usize) -> Vec<Anomaly> {
let detector = ANOMALY_DETECTOR.lock();
detector.get_recent_anomalies(limit)
}
pub fn stats() -> AnomalyStats {
let detector = ANOMALY_DETECTOR.lock();
detector.get_stats()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_anomaly_severity() {
assert!(AnomalyType::MassEncryption.severity() > AnomalyType::UnusualTime.severity());
assert!(AnomalyType::MassDeletion.severity() > AnomalyType::HighEntropy.severity());
}
#[test]
fn test_baseline_creation() {
let baseline = BehaviorBaseline::new(1);
assert_eq!(baseline.user_id, 1);
assert_eq!(baseline.sample_count, 0);
}
#[test]
fn test_welford_update() {
let mut baseline = BehaviorBaseline::new(1);
for &val in &[10.0, 12.0, 11.0, 13.0, 9.0] {
baseline.update_creates(val);
}
assert!((baseline.avg_creates_per_hour - 11.0).abs() < 0.1);
let stddev = baseline.creates_stddev();
assert!(stddev > 1.0 && stddev < 2.0);
}
#[test]
fn test_three_sigma_detection() {
let mut baseline = BehaviorBaseline::new(1);
for _ in 0..20 {
baseline.update_creates(10.0);
}
assert!(!baseline.is_creates_anomalous(10.0));
assert!(!baseline.is_creates_anomalous(12.0));
assert!(baseline.is_creates_anomalous(50.0));
}
#[test]
fn test_active_hours() {
let mut baseline = BehaviorBaseline::new(1);
baseline.mark_active_hour(9); baseline.mark_active_hour(10);
baseline.mark_active_hour(14);
assert!(baseline.is_typical_hour(9));
assert!(baseline.is_typical_hour(10));
assert!(!baseline.is_typical_hour(3)); }
#[test]
fn test_rapid_creation_detection() {
let mut detector = AnomalyDetector::new();
for i in 0..110 {
let result = detector.record_create(1, 100, 1000 + i);
if i >= 100 {
assert!(result.is_some()); }
}
}
#[test]
fn test_mass_deletion_detection() {
let mut detector = AnomalyDetector::new();
for i in 0..60 {
let result = detector.record_delete(1, 100, 1000 + i);
if i >= 50 {
assert!(result.is_some());
}
}
}
#[test]
fn test_high_entropy_detection() {
let mut detector = AnomalyDetector::new();
assert!(detector.check_entropy(1, 100, 1000, 4.5).is_none());
assert!(detector.check_entropy(1, 100, 1001, 7.8).is_some());
}
#[test]
fn test_ransomware_detection() {
let mut detector = AnomalyDetector::new();
for i in 0..60 {
detector.record_create(1, 100, 1000 + i);
detector.record_delete(1, 100, 1000 + i);
}
let result = detector.detect_ransomware(1, 100, 1060);
assert!(result.is_some());
let anomaly = result.expect("test: operation should succeed");
assert_eq!(anomaly.anomaly_type, AnomalyType::MassEncryption);
assert!(anomaly.severity > 90);
}
#[test]
fn test_statistics() {
let mut detector = AnomalyDetector::new();
for i in 0..110 {
detector.record_create(1, 100, 1000 + i);
}
let stats = detector.get_stats();
assert!(stats.total_anomalies > 0);
assert!(stats.by_type.contains_key("rapid_creation"));
}
}