kindly_guard_server/plugins/
manager.rs

1// Copyright 2025 Kindly Software Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14//! Plugin manager implementation
15//!
16//! Manages the lifecycle of security plugins and coordinates scanning
17
18use 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
28/// Default plugin manager implementation
29pub struct DefaultPluginManager {
30    config: PluginConfig,
31    plugins: Arc<RwLock<HashMap<PluginId, PluginHandle>>>,
32    loaders: Vec<Box<dyn PluginLoader>>,
33}
34
35impl DefaultPluginManager {
36    /// Create a new plugin manager
37    pub fn new(config: PluginConfig) -> Result<Self> {
38        let loaders: Vec<Box<dyn PluginLoader>> = vec![Box::new(NativePluginLoader::new())];
39
40        // WASM support planned for future release
41        // #[cfg(feature = "wasm")]
42        // if config.wasm_enabled {
43        //     loaders.push(Box::new(WasmPluginLoader::new()?));
44        // }
45
46        let manager = Self {
47            config,
48            plugins: Arc::new(RwLock::new(HashMap::new())),
49            loaders,
50        };
51
52        Ok(manager)
53    }
54
55    /// Initialize and auto-load plugins
56    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    /// Load all plugins from a directory
81    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            // Skip non-files
91            if !path.is_file() {
92                continue;
93            }
94
95            // Try to load as plugin
96            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    /// Check if plugin is allowed
110    fn is_plugin_allowed(&self, metadata: &PluginMetadata) -> bool {
111        // Check denylist first
112        if self.config.denylist.contains(&metadata.name) {
113            return false;
114        }
115
116        // Check allowlist if not empty
117        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        // Check if allowed
130        if !self.is_plugin_allowed(&metadata) {
131            return Err(anyhow::anyhow!("Plugin '{}' is not allowed", metadata.name));
132        }
133
134        // Initialize plugin
135        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        // Try each loader
162        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            // Shutdown plugin
193            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            // Apply timeout
243            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                    // Update metrics
251                    {
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                    // Update error count
274                    {
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                    // Update error count
283                    {
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        // Apply timeout
308        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        // Update metrics
319        {
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        // Add metrics from handle
341        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        // For now, reload is not implemented for in-memory plugins
349        // This would require storing the original path and reloading from disk
350        Err(anyhow::anyhow!(
351            "Hot reload not implemented for this plugin type"
352        ))
353    }
354}