kindly_guard_server/plugins/
manager.rs1use super::{
19 async_trait, HealthStatus, NativePluginLoader, Path, PluginConfig, PluginHandle, PluginId,
20 PluginInfo, PluginLoader, PluginManagerTrait, PluginMetadata, PluginMetrics, Result,
21 ScanContext, SecurityPlugin, Threat,
22};
23use std::collections::HashMap;
24use std::sync::Arc;
25use tokio::sync::RwLock;
26use tracing::{debug, error, info, warn};
27
28pub struct DefaultPluginManager {
30 config: PluginConfig,
31 plugins: Arc<RwLock<HashMap<PluginId, PluginHandle>>>,
32 loaders: Vec<Box<dyn PluginLoader>>,
33}
34
35impl DefaultPluginManager {
36 pub fn new(config: PluginConfig) -> Result<Self> {
38 let loaders: Vec<Box<dyn PluginLoader>> = vec![Box::new(NativePluginLoader::new())];
39
40 let manager = Self {
47 config,
48 plugins: Arc::new(RwLock::new(HashMap::new())),
49 loaders,
50 };
51
52 Ok(manager)
53 }
54
55 pub async fn initialize(&self) -> Result<()> {
57 if !self.config.enabled {
58 info!("Plugin system is disabled");
59 return Ok(());
60 }
61
62 info!("Initializing plugin system");
63
64 if self.config.auto_load {
65 for dir in &self.config.plugin_dirs {
66 if dir.exists() {
67 self.load_plugins_from_directory(dir).await?;
68 } else {
69 warn!("Plugin directory does not exist: {:?}", dir);
70 }
71 }
72 }
73
74 let plugins = self.plugins.read().await;
75 info!("Loaded {} plugins", plugins.len());
76
77 Ok(())
78 }
79
80 pub async fn load_plugins_from_directory(&self, dir: &Path) -> Result<()> {
82 debug!("Scanning directory for plugins: {:?}", dir);
83
84 let entries = std::fs::read_dir(dir)?;
85
86 for entry in entries {
87 let entry = entry?;
88 let path = entry.path();
89
90 if !path.is_file() {
92 continue;
93 }
94
95 match self.load_plugin(&path).await {
97 Ok(id) => {
98 info!("Loaded plugin {} from {:?}", id.0, path);
99 },
100 Err(e) => {
101 warn!("Failed to load plugin from {:?}: {}", path, e);
102 },
103 }
104 }
105
106 Ok(())
107 }
108
109 fn is_plugin_allowed(&self, metadata: &PluginMetadata) -> bool {
111 if self.config.denylist.contains(&metadata.name) {
113 return false;
114 }
115
116 if !self.config.allowlist.is_empty() {
118 return self.config.allowlist.contains(&metadata.name);
119 }
120
121 true
122 }
123}
124
125impl DefaultPluginManager {
126 async fn register_plugin(&self, mut plugin: Box<dyn SecurityPlugin>) -> Result<PluginId> {
127 let metadata = plugin.metadata();
128
129 if !self.is_plugin_allowed(&metadata) {
131 return Err(anyhow::anyhow!("Plugin '{}' is not allowed", metadata.name));
132 }
133
134 plugin
136 .initialize(serde_json::Value::Object(Default::default()))
137 .await?;
138
139 let id = PluginId::new();
140 let handle = PluginHandle {
141 id: id.clone(),
142 metadata: metadata.clone(),
143 plugin: Arc::from(plugin),
144 enabled: true,
145 loaded_at: chrono::Utc::now(),
146 metrics: Arc::new(RwLock::new(PluginMetrics::default())),
147 };
148
149 let mut plugins = self.plugins.write().await;
150 plugins.insert(id.clone(), handle);
151
152 info!("Registered plugin: {} v{}", metadata.name, metadata.version);
153
154 Ok(id)
155 }
156}
157
158#[async_trait]
159impl PluginManagerTrait for DefaultPluginManager {
160 async fn load_plugin(&self, path: &Path) -> Result<PluginId> {
161 for loader in &self.loaders {
163 match loader.validate_plugin(path).await {
164 Ok(metadata) => {
165 debug!(
166 "Plugin validated by {} loader: {}",
167 loader.loader_type(),
168 metadata.name
169 );
170
171 if !self.is_plugin_allowed(&metadata) {
172 return Err(anyhow::anyhow!("Plugin '{}' is not allowed", metadata.name));
173 }
174
175 let plugin = loader.load_plugin(path).await?;
176 return self.register_plugin(plugin).await;
177 },
178 Err(_) => continue,
179 }
180 }
181
182 Err(anyhow::anyhow!(
183 "No loader could handle plugin at {:?}",
184 path
185 ))
186 }
187
188 async fn unload_plugin(&self, id: &PluginId) -> Result<()> {
189 let mut plugins = self.plugins.write().await;
190
191 if let Some(mut handle) = plugins.remove(id) {
192 if let Some(plugin) = Arc::get_mut(&mut handle.plugin) {
194 plugin.shutdown().await?;
195 }
196
197 info!("Unloaded plugin: {}", handle.metadata.name);
198 Ok(())
199 } else {
200 Err(anyhow::anyhow!("Plugin not found: {}", id.0))
201 }
202 }
203
204 async fn get_plugin(&self, id: &PluginId) -> Result<PluginInfo> {
205 let plugins = self.plugins.read().await;
206 let handle = plugins
207 .get(id)
208 .ok_or_else(|| anyhow::anyhow!("Plugin not found: {}", id.0))?;
209
210 Ok(PluginInfo {
211 id: handle.id.clone(),
212 metadata: handle.metadata.clone(),
213 enabled: handle.enabled,
214 loaded_at: handle.loaded_at,
215 })
216 }
217
218 async fn list_plugins(&self) -> Result<Vec<PluginInfo>> {
219 let plugins = self.plugins.read().await;
220 Ok(plugins
221 .values()
222 .map(|handle| PluginInfo {
223 id: handle.id.clone(),
224 metadata: handle.metadata.clone(),
225 enabled: handle.enabled,
226 loaded_at: handle.loaded_at,
227 })
228 .collect())
229 }
230
231 async fn scan_all(&self, context: ScanContext<'_>) -> Result<HashMap<PluginId, Vec<Threat>>> {
232 let plugins = self.plugins.read().await;
233 let mut results = HashMap::new();
234
235 for (id, handle) in plugins.iter() {
236 if !handle.enabled {
237 continue;
238 }
239
240 let start = std::time::Instant::now();
241
242 let scan_future = handle.plugin.scan(context.clone());
244 let timeout = tokio::time::Duration::from_millis(self.config.max_execution_time_ms);
245
246 match tokio::time::timeout(timeout, scan_future).await {
247 Ok(Ok(threats)) => {
248 let elapsed = start.elapsed().as_micros() as u64;
249
250 {
252 let mut metrics = handle.metrics.write().await;
253 metrics.scans_performed += 1;
254 metrics.threats_detected += threats.len() as u64;
255 metrics.avg_scan_time_us =
256 (metrics.avg_scan_time_us * (metrics.scans_performed - 1) + elapsed)
257 / metrics.scans_performed;
258 }
259
260 if !threats.is_empty() {
261 debug!(
262 "Plugin {} found {} threats",
263 handle.metadata.name,
264 threats.len()
265 );
266 }
267
268 results.insert(id.clone(), threats);
269 },
270 Ok(Err(e)) => {
271 error!("Plugin {} scan error: {}", handle.metadata.name, e);
272
273 {
275 let mut metrics = handle.metrics.write().await;
276 metrics.errors += 1;
277 }
278 },
279 Err(_) => {
280 error!("Plugin {} scan timeout", handle.metadata.name);
281
282 {
284 let mut metrics = handle.metrics.write().await;
285 metrics.errors += 1;
286 }
287 },
288 }
289 }
290
291 Ok(results)
292 }
293
294 async fn scan(&self, id: &PluginId, context: ScanContext<'_>) -> Result<Vec<Threat>> {
295 let plugins = self.plugins.read().await;
296
297 let handle = plugins
298 .get(id)
299 .ok_or_else(|| anyhow::anyhow!("Plugin not found: {}", id.0))?;
300
301 if !handle.enabled {
302 return Err(anyhow::anyhow!("Plugin is disabled: {}", id.0));
303 }
304
305 let start = std::time::Instant::now();
306
307 let scan_future = handle.plugin.scan(context);
309 let timeout = tokio::time::Duration::from_millis(self.config.max_execution_time_ms);
310
311 let result = tokio::time::timeout(timeout, scan_future)
312 .await
313 .map_err(|_| anyhow::anyhow!("Plugin scan timeout"))?;
314
315 let threats = result?;
316 let elapsed = start.elapsed().as_micros() as u64;
317
318 {
320 let mut metrics = handle.metrics.write().await;
321 metrics.scans_performed += 1;
322 metrics.threats_detected += threats.len() as u64;
323 metrics.avg_scan_time_us = (metrics.avg_scan_time_us * (metrics.scans_performed - 1)
324 + elapsed)
325 / metrics.scans_performed;
326 }
327
328 Ok(threats)
329 }
330
331 async fn get_health(&self, id: &PluginId) -> Result<HealthStatus> {
332 let plugins = self.plugins.read().await;
333
334 let handle = plugins
335 .get(id)
336 .ok_or_else(|| anyhow::anyhow!("Plugin not found: {}", id.0))?;
337
338 let mut health = handle.plugin.health_check().await?;
339
340 let metrics = handle.metrics.read().await;
342 health.metrics = (*metrics).clone();
343
344 Ok(health)
345 }
346
347 async fn reload_plugin(&self, _id: &PluginId) -> Result<()> {
348 Err(anyhow::anyhow!(
351 "Hot reload not implemented for this plugin type"
352 ))
353 }
354}