kindly_guard_server/shield/
mod.rs1use anyhow::Result;
17use std::collections::VecDeque;
18use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
19use std::sync::{Arc, Mutex, Weak};
20use std::time::{Duration, Instant};
21use tracing::error;
22
23use crate::config::ShieldConfig;
24use crate::scanner::Threat;
25use crate::traits::SecurityEventProcessor;
26
27pub mod cli;
28pub mod display;
29pub mod universal_display;
30
31pub use cli::{CliShield, DisplayFormat, ShieldStatus};
32pub use display::ShieldDisplay;
33pub use universal_display::{UniversalDisplay, UniversalDisplayConfig, UniversalShieldStatus};
34
35pub struct Shield {
37 active: AtomicBool,
38 start_time: Instant,
39 threats_blocked: AtomicU64,
40 recent_threats: Arc<Mutex<VecDeque<TimestampedThreat>>>,
41 config: ShieldConfig,
42 event_processor_enabled: AtomicBool,
44 event_processor: Mutex<Option<Weak<dyn SecurityEventProcessor>>>,
46}
47
48#[derive(Clone)]
50struct TimestampedThreat {
51 threat: Threat,
52 timestamp: Instant,
53}
54
55#[derive(serde::Serialize, serde::Deserialize)]
57pub struct ShieldInfo {
58 pub active: bool,
59 #[serde(with = "serde_duration")]
60 pub uptime: Duration,
61 pub threats_blocked: u64,
62 pub recent_threat_rate: f64,
63}
64
65mod serde_duration {
67 use serde::{Deserialize, Deserializer, Serializer};
68 use std::time::Duration;
69
70 pub fn serialize<S>(duration: &Duration, serializer: S) -> Result<S::Ok, S::Error>
71 where
72 S: Serializer,
73 {
74 serializer.serialize_u64(duration.as_secs())
75 }
76
77 pub fn deserialize<'de, D>(deserializer: D) -> Result<Duration, D::Error>
78 where
79 D: Deserializer<'de>,
80 {
81 let secs = u64::deserialize(deserializer)?;
82 Ok(Duration::from_secs(secs))
83 }
84}
85
86pub struct ShieldStats {
88 pub threats_blocked: u64,
89 pub active: bool,
90}
91
92impl Default for Shield {
93 fn default() -> Self {
94 Self::new()
95 }
96}
97
98impl Shield {
99 pub fn new() -> Self {
101 Self::with_config(ShieldConfig::default())
102 }
103
104 pub fn with_config(config: ShieldConfig) -> Self {
106 Self {
107 active: AtomicBool::new(false),
108 start_time: Instant::now(),
109 threats_blocked: AtomicU64::new(0),
110 recent_threats: Arc::new(Mutex::new(VecDeque::with_capacity(1000))),
111 config,
112 event_processor_enabled: AtomicBool::new(false),
113 event_processor: Mutex::new(None),
114 }
115 }
116
117 pub fn set_active(&self, active: bool) {
119 self.active.store(active, Ordering::Relaxed);
120 }
121
122 pub fn is_active(&self) -> bool {
124 self.active.load(Ordering::Relaxed)
125 }
126
127 pub fn set_event_processor_enabled(&self, enabled: bool) {
129 self.event_processor_enabled
130 .store(enabled, Ordering::Relaxed);
131 }
132
133 pub fn is_event_processor_enabled(&self) -> bool {
135 self.event_processor_enabled.load(Ordering::Relaxed)
136 }
137
138 pub fn set_event_processor(&self, processor: &Arc<dyn SecurityEventProcessor>) {
140 match self.event_processor.lock() {
141 Ok(mut ep) => *ep = Some(Arc::downgrade(processor)),
142 Err(e) => error!("Failed to acquire event processor lock: {}", e),
143 }
144 }
145
146 pub fn record_threats(&self, threats: &[Threat]) {
148 if threats.is_empty() {
149 return;
150 }
151
152 let count = threats.len() as u64;
153 self.threats_blocked.fetch_add(count, Ordering::Relaxed);
154
155 let now = Instant::now();
156 let Ok(mut recent) = self.recent_threats.lock() else {
157 error!("Failed to acquire recent threats lock");
158 return;
159 };
160
161 for threat in threats {
162 recent.push_back(TimestampedThreat {
164 threat: threat.clone(),
165 timestamp: now,
166 });
167
168 while recent.len() > 1000 {
170 recent.pop_front();
171 }
172 }
173 }
174
175 pub fn get_info(&self) -> ShieldInfo {
177 let now = Instant::now();
178 let uptime = now.duration_since(self.start_time);
179
180 let recent_rate = match self.recent_threats.lock() {
182 Ok(recent) => {
183 let five_mins_ago = now.checked_sub(Duration::from_secs(300)).unwrap_or(now); let recent_count = recent
185 .iter()
186 .filter(|t| t.timestamp > five_mins_ago)
187 .count() as f64;
188 recent_count / 5.0 },
190 Err(e) => {
191 error!("Failed to acquire recent threats lock: {}", e);
192 0.0 },
194 };
195
196 if self.is_event_processor_enabled() {
198 if let Ok(ep_lock) = self.event_processor.lock() {
199 if let Some(weak_proc) = ep_lock.as_ref() {
200 if let Some(processor) = weak_proc.upgrade() {
201 if processor.is_monitored("any") {
203 tracing::trace!("Attack pattern correlation active");
204 }
205 }
206 }
207 }
208 }
209
210 ShieldInfo {
211 active: self.is_active(),
212 uptime,
213 threats_blocked: self.threats_blocked.load(Ordering::Relaxed),
214 recent_threat_rate: recent_rate,
215 }
216 }
217
218 pub fn get_recent_threats(&self, limit: usize) -> Vec<Threat> {
220 match self.recent_threats.lock() {
221 Ok(recent) => recent
222 .iter()
223 .rev()
224 .take(limit)
225 .map(|t| t.threat.clone())
226 .collect(),
227 Err(e) => {
228 error!("Failed to acquire recent threats lock: {}", e);
229 vec![]
230 },
231 }
232 }
233
234 pub fn get_threat_stats(&self) -> std::collections::HashMap<crate::scanner::ThreatType, u64> {
236 use std::collections::HashMap;
237
238 match self.recent_threats.lock() {
239 Ok(recent) => {
240 let mut stats = HashMap::new();
241 for item in recent.iter() {
242 *stats.entry(item.threat.threat_type.clone()).or_insert(0) += 1;
243 }
244 stats
245 },
246 Err(e) => {
247 error!("Failed to acquire recent threats lock: {}", e);
248 HashMap::new()
249 },
250 }
251 }
252
253 pub async fn start_display(self: Arc<Self>) -> Result<()> {
255 if !self.config.enabled {
256 return Ok(());
257 }
258
259 let display = ShieldDisplay::new(self.clone(), self.config.clone());
260 display.run().await
261 }
262
263 pub const fn start_time(&self) -> Instant {
265 self.start_time
266 }
267
268 pub fn stats(&self) -> ShieldStats {
270 ShieldStats {
271 threats_blocked: self.threats_blocked.load(Ordering::Relaxed),
272 active: self.is_active(),
273 }
274 }
275
276 pub fn last_threat_type(&self) -> Option<String> {
278 match self.recent_threats.lock() {
279 Ok(recent) => recent.back().map(|t| format!("{}", t.threat.threat_type)),
280 Err(e) => {
281 error!("Failed to acquire recent threats lock: {}", e);
282 None
283 },
284 }
285 }
286
287 pub fn scanner_stats(&self) -> crate::scanner::ScannerStats {
289 match self.recent_threats.lock() {
291 Ok(threats) => {
292 let (unicode_count, injection_count) =
293 threats
294 .iter()
295 .fold((0u64, 0u64), |(unicode, injection), item| {
296 match &item.threat.threat_type {
297 crate::scanner::ThreatType::UnicodeInvisible
298 | crate::scanner::ThreatType::UnicodeBiDi
299 | crate::scanner::ThreatType::UnicodeHomograph => {
300 (unicode + 1, injection)
301 },
302
303 crate::scanner::ThreatType::SqlInjection
304 | crate::scanner::ThreatType::CommandInjection
305 | crate::scanner::ThreatType::PromptInjection
306 | crate::scanner::ThreatType::PathTraversal => {
307 (unicode, injection + 1)
308 },
309
310 _ => (unicode, injection),
311 }
312 });
313
314 crate::scanner::ScannerStats {
315 unicode_threats_detected: unicode_count,
316 injection_threats_detected: injection_count,
317 total_scans: self.threats_blocked.load(Ordering::Relaxed),
318 }
319 },
320 Err(_) => crate::scanner::ScannerStats {
321 unicode_threats_detected: 0,
322 injection_threats_detected: 0,
323 total_scans: 0,
324 },
325 }
326 }
327
328 pub fn set_enabled(&self, enabled: bool) {
330 if enabled {
333 tracing::info!("Shield display enabled");
334 self.set_active(true);
335 } else {
336 tracing::info!("Shield display disabled");
337 self.set_active(false);
338 }
339 }
340}