Skip to main content

ferrox_security/
threats.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use tokio::sync::RwLock;
4use std::time::{Duration, Instant};
5
6#[derive(Debug, Clone, Serialize, Deserialize)]
7pub struct SecurityThreat {
8    pub id: String,
9    pub ip: String,
10    pub threat_type: String,
11    pub timestamp_rfc3339: String,
12    pub details: String,
13    pub strikes: u32,
14    pub is_banned: bool,
15}
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct SecurityTelemetrySummary {
19    pub total_threats_detected: u64,
20    pub active_banned_ips_count: usize,
21    pub brute_force_attempts_today: u64,
22    pub rate_limit_violations_today: u64,
23    pub active_banned_ips: Vec<String>,
24    pub recent_threats: Vec<SecurityThreat>,
25}
26
27struct IpTracker {
28    failed_auth_count: u32,
29    request_timestamps: Vec<Instant>,
30    banned_until: Option<Instant>,
31    total_strikes: u32,
32}
33
34pub struct SecurityManager {
35    ip_records: RwLock<HashMap<String, IpTracker>>,
36    threat_log: RwLock<Vec<SecurityThreat>>,
37    total_threats_detected: RwLock<u64>,
38    brute_force_attempts_today: RwLock<u64>,
39    rate_limit_violations_today: RwLock<u64>,
40}
41
42impl SecurityManager {
43    pub fn new() -> Self {
44        Self {
45            ip_records: RwLock::new(HashMap::new()),
46            threat_log: RwLock::new(Vec::new()),
47            total_threats_detected: RwLock::new(0),
48            brute_force_attempts_today: RwLock::new(0),
49            rate_limit_violations_today: RwLock::new(0),
50        }
51    }
52
53    pub async fn is_ip_banned(&self, ip: &str) -> bool {
54        let mut records = self.ip_records.write().await;
55        if let Some(tracker) = records.get_mut(ip) {
56            if let Some(banned_until) = tracker.banned_until {
57                if Instant::now() < banned_until {
58                    return true;
59                } else {
60                    // Lockout period expired
61                    tracker.banned_until = None;
62                }
63            }
64        }
65        false
66    }
67
68    pub async fn record_request(&self, ip: &str) -> Result<(), &'static str> {
69        if self.is_ip_banned(ip).await {
70            return Err("IP address is temporarily banned due to security violations");
71        }
72
73        let now = Instant::now();
74        let mut records = self.ip_records.write().await;
75        let tracker = records.entry(ip.to_string()).or_insert_with(|| IpTracker {
76            failed_auth_count: 0,
77            request_timestamps: Vec::new(),
78            banned_until: None,
79            total_strikes: 0,
80        });
81
82        // Retain request timestamps within the last 10 seconds window
83        tracker.request_timestamps.retain(|&t| now.duration_since(t) < Duration::from_secs(10));
84        tracker.request_timestamps.push(now);
85
86        // Burst rate limit check: > 50 requests in 10 seconds triggers a rate limit violation strike
87        if tracker.request_timestamps.len() > 50 {
88            tracker.total_strikes += 1;
89            *self.rate_limit_violations_today.write().await += 1;
90            *self.total_threats_detected.write().await += 1;
91
92            let threat = SecurityThreat {
93                id: uuid::Uuid::new_v4().to_string(),
94                ip: ip.to_string(),
95                threat_type: "RateLimitExceeded".to_string(),
96                timestamp_rfc3339: chrono::Utc::now().to_rfc3339(),
97                details: format!("Excessive burst request rate: {} requests in 10s", tracker.request_timestamps.len()),
98                strikes: tracker.total_strikes,
99                is_banned: false,
100            };
101
102            self.add_threat_log(threat).await;
103
104            if tracker.total_strikes >= 3 {
105                tracker.banned_until = Some(now + Duration::from_secs(900)); // 15 min ban
106                return Err("Rate limit threshold exceeded. IP temporarily banned for 15 minutes.");
107            }
108        }
109
110        Ok(())
111    }
112
113    pub async fn record_auth_failure(&self, ip: &str, username_or_email: &str) {
114        let now = Instant::now();
115        let mut records = self.ip_records.write().await;
116        let tracker = records.entry(ip.to_string()).or_insert_with(|| IpTracker {
117            failed_auth_count: 0,
118            request_timestamps: Vec::new(),
119            banned_until: None,
120            total_strikes: 0,
121        });
122
123        tracker.failed_auth_count += 1;
124        tracker.total_strikes += 1;
125        *self.brute_force_attempts_today.write().await += 1;
126        *self.total_threats_detected.write().await += 1;
127
128        let should_ban = tracker.failed_auth_count >= 5;
129        if should_ban {
130            tracker.banned_until = Some(now + Duration::from_secs(1800)); // 30 min ban for brute force
131        }
132
133        let threat = SecurityThreat {
134            id: uuid::Uuid::new_v4().to_string(),
135            ip: ip.to_string(),
136            threat_type: "BruteForceAttempt".to_string(),
137            timestamp_rfc3339: chrono::Utc::now().to_rfc3339(),
138            details: format!("Failed authentication attempt #{} for target: '{}'", tracker.failed_auth_count, username_or_email),
139            strikes: tracker.total_strikes,
140            is_banned: should_ban,
141        };
142
143        drop(records);
144        self.add_threat_log(threat).await;
145    }
146
147    pub async fn record_auth_success(&self, ip: &str) {
148        let mut records = self.ip_records.write().await;
149        if let Some(tracker) = records.get_mut(ip) {
150            tracker.failed_auth_count = 0;
151        }
152    }
153
154    pub async fn ban_ip(&self, ip: &str, duration_minutes: u64) {
155        let mut records = self.ip_records.write().await;
156        let tracker = records.entry(ip.to_string()).or_insert_with(|| IpTracker {
157            failed_auth_count: 0,
158            request_timestamps: Vec::new(),
159            banned_until: None,
160            total_strikes: 0,
161        });
162
163        tracker.banned_until = Some(Instant::now() + Duration::from_secs(duration_minutes * 60));
164        tracker.total_strikes += 5;
165
166        let threat = SecurityThreat {
167            id: uuid::Uuid::new_v4().to_string(),
168            ip: ip.to_string(),
169            threat_type: "ManualAdminBan".to_string(),
170            timestamp_rfc3339: chrono::Utc::now().to_rfc3339(),
171            details: format!("IP address manually banned by administrator for {} minutes", duration_minutes),
172            strikes: tracker.total_strikes,
173            is_banned: true,
174        };
175
176        drop(records);
177        self.add_threat_log(threat).await;
178    }
179
180    pub async fn unblock_ip(&self, ip: &str) -> bool {
181        let mut records = self.ip_records.write().await;
182        if let Some(tracker) = records.get_mut(ip) {
183            tracker.banned_until = None;
184            tracker.failed_auth_count = 0;
185            return true;
186        }
187        false
188    }
189
190    pub async fn get_summary(&self) -> SecurityTelemetrySummary {
191        let records = self.ip_records.read().await;
192        let threat_log = self.threat_log.read().await;
193        let now = Instant::now();
194
195        let active_banned_ips: Vec<String> = records.iter()
196            .filter(|(_, tracker)| tracker.banned_until.map_or(false, |until| now < until))
197            .map(|(ip, _)| ip.clone())
198            .collect();
199
200        let active_banned_count = active_banned_ips.len();
201        let recent_threats = threat_log.iter().rev().take(20).cloned().collect();
202
203        SecurityTelemetrySummary {
204            total_threats_detected: *self.total_threats_detected.read().await,
205            active_banned_ips_count: active_banned_count,
206            brute_force_attempts_today: *self.brute_force_attempts_today.read().await,
207            rate_limit_violations_today: *self.rate_limit_violations_today.read().await,
208            active_banned_ips,
209            recent_threats,
210        }
211    }
212
213    async fn add_threat_log(&self, threat: SecurityThreat) {
214        let mut log = self.threat_log.write().await;
215        if log.len() >= 100 {
216            log.remove(0);
217        }
218        log.push(threat.clone());
219        drop(log);
220
221        // Dispath alert to Discord Webhook if configured
222        tokio::spawn(async move {
223            if let Ok(webhook_url) = std::env::var("DISCORD_WEBHOOK_URL") {
224                let client = reqwest::Client::new();
225                let color = if threat.is_banned { 0xE74C3C } else { 0xF39C12 };
226                
227                let payload = serde_json::json!({
228                    "username": "Ferrox Security Mesh",
229                    "avatar_url": "https://i.imgur.com/4M34hi2.png", // Optional shield icon
230                    "embeds": [{
231                        "title": format!("🚨 Security Alert: {}", threat.threat_type),
232                        "description": format!("**IP:** {}\n**Details:** {}\n**Strikes:** {}\n**Banned:** {}", 
233                            threat.ip, threat.details, threat.strikes, threat.is_banned),
234                        "color": color,
235                        "timestamp": chrono::Utc::now().to_rfc3339()
236                    }]
237                });
238
239                let _ = client.post(&webhook_url)
240                    .json(&payload)
241                    .send()
242                    .await;
243            }
244        });
245    }
246}