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 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 tracker.request_timestamps.retain(|&t| now.duration_since(t) < Duration::from_secs(10));
84 tracker.request_timestamps.push(now);
85
86 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)); 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)); }
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 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", "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}