kindly_guard_server/plugins/
mod.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 system for extensible security scanning
15//!
16//! This module provides a trait-based plugin architecture that allows
17//! users to extend `KindlyGuard` with custom security scanners without
18//! modifying the core code.
19
20use anyhow::Result;
21use async_trait::async_trait;
22use serde::{Deserialize, Serialize};
23use std::collections::HashMap;
24use std::path::{Path, PathBuf};
25use std::sync::Arc;
26use uuid::Uuid;
27
28pub mod manager;
29pub mod native;
30// WASM support planned for future release
31// #[cfg(feature = "wasm")]
32// pub mod wasm;
33
34// Re-exports
35pub use manager::DefaultPluginManager;
36pub use native::NativePluginLoader;
37// #[cfg(feature = "wasm")]
38// pub use wasm::WasmPluginLoader;
39
40use crate::scanner::{Severity, Threat, ThreatType};
41
42/// Unique plugin identifier
43#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
44pub struct PluginId(pub String);
45
46impl Default for PluginId {
47    fn default() -> Self {
48        Self::new()
49    }
50}
51
52impl PluginId {
53    /// Create a new random plugin ID
54    pub fn new() -> Self {
55        Self(Uuid::new_v4().to_string())
56    }
57
58    /// Create from a string
59    pub const fn from_string(s: String) -> Self {
60        Self(s)
61    }
62}
63
64/// Plugin metadata
65#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct PluginMetadata {
67    /// Plugin name
68    pub name: String,
69    /// Plugin version
70    pub version: String,
71    /// Plugin author
72    pub author: String,
73    /// Plugin description
74    pub description: String,
75    /// Plugin homepage
76    pub homepage: Option<String>,
77    /// Supported threat types
78    pub threat_types: Vec<String>,
79    /// Plugin capabilities
80    pub capabilities: PluginCapabilities,
81}
82
83/// Plugin capabilities
84#[derive(Debug, Clone, Default, Serialize, Deserialize)]
85pub struct PluginCapabilities {
86    /// Can scan text
87    pub scan_text: bool,
88    /// Can scan JSON
89    pub scan_json: bool,
90    /// Can scan binary data
91    pub scan_binary: bool,
92    /// Supports async scanning
93    pub async_scan: bool,
94    /// Supports batch scanning
95    pub batch_scan: bool,
96    /// Maximum data size in MB
97    pub max_data_size_mb: Option<u32>,
98}
99
100/// Scan context provided to plugins
101#[derive(Debug, Clone)]
102pub struct ScanContext<'a> {
103    /// Data to scan
104    pub data: &'a [u8],
105    /// Content type hint
106    pub content_type: Option<&'a str>,
107    /// Client ID
108    pub client_id: &'a str,
109    /// Request metadata
110    pub metadata: &'a HashMap<String, String>,
111    /// Scan options
112    pub options: ScanOptions,
113}
114
115/// Scan options
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
117pub struct ScanOptions {
118    /// Maximum scan depth
119    pub max_depth: Option<u32>,
120    /// Timeout in milliseconds
121    pub timeout_ms: Option<u64>,
122    /// Enable detailed reporting
123    pub detailed: bool,
124    /// Custom plugin options
125    pub plugin_options: HashMap<String, serde_json::Value>,
126}
127
128/// Plugin health status
129#[derive(Debug, Clone, Serialize, Deserialize)]
130pub struct HealthStatus {
131    /// Is healthy
132    pub healthy: bool,
133    /// Status message
134    pub message: String,
135    /// Last check time
136    pub last_check: chrono::DateTime<chrono::Utc>,
137    /// Performance metrics
138    pub metrics: PluginMetrics,
139}
140
141/// Plugin performance metrics
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
143pub struct PluginMetrics {
144    /// Total scans performed
145    pub scans_performed: u64,
146    /// Total threats detected
147    pub threats_detected: u64,
148    /// Average scan time in microseconds
149    pub avg_scan_time_us: u64,
150    /// Errors encountered
151    pub errors: u64,
152}
153
154/// Security plugin trait
155#[async_trait]
156pub trait SecurityPlugin: Send + Sync {
157    /// Get plugin metadata
158    fn metadata(&self) -> PluginMetadata;
159
160    /// Initialize plugin with configuration
161    async fn initialize(&mut self, config: serde_json::Value) -> Result<()>;
162
163    /// Scan data for threats
164    async fn scan(&self, context: ScanContext<'_>) -> Result<Vec<Threat>>;
165
166    /// Perform health check
167    async fn health_check(&self) -> Result<HealthStatus>;
168
169    /// Shutdown plugin
170    async fn shutdown(&mut self) -> Result<()>;
171
172    /// Update plugin configuration
173    async fn update_config(&mut self, config: serde_json::Value) -> Result<()> {
174        // Default implementation reinitializes
175        self.shutdown().await?;
176        self.initialize(config).await
177    }
178
179    /// Get current metrics
180    fn get_metrics(&self) -> PluginMetrics {
181        PluginMetrics::default()
182    }
183}
184
185/// Plugin loader trait for different plugin types
186#[async_trait]
187pub trait PluginLoader: Send + Sync {
188    /// Load a plugin from path
189    async fn load_plugin(&self, path: &Path) -> Result<Box<dyn SecurityPlugin>>;
190
191    /// Validate plugin before loading
192    async fn validate_plugin(&self, path: &Path) -> Result<PluginMetadata>;
193
194    /// Get loader type
195    fn loader_type(&self) -> &'static str;
196}
197
198/// Plugin info returned by manager
199#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct PluginInfo {
201    pub id: PluginId,
202    pub metadata: PluginMetadata,
203    pub enabled: bool,
204    pub loaded_at: chrono::DateTime<chrono::Utc>,
205}
206
207/// Plugin manager trait
208#[async_trait]
209pub trait PluginManagerTrait: Send + Sync {
210    /// Load plugin from file
211    async fn load_plugin(&self, path: &Path) -> Result<PluginId>;
212
213    /// Unload a plugin
214    async fn unload_plugin(&self, id: &PluginId) -> Result<()>;
215
216    /// Get plugin info
217    async fn get_plugin(&self, id: &PluginId) -> Result<PluginInfo>;
218
219    /// List all plugins
220    async fn list_plugins(&self) -> Result<Vec<PluginInfo>>;
221
222    /// Scan with specific plugin
223    async fn scan(&self, id: &PluginId, context: ScanContext<'_>) -> Result<Vec<Threat>>;
224
225    /// Scan with all plugins
226    async fn scan_all(&self, context: ScanContext<'_>) -> Result<HashMap<PluginId, Vec<Threat>>>;
227
228    /// Reload a plugin
229    async fn reload_plugin(&self, id: &PluginId) -> Result<()>;
230
231    /// Get plugin health status
232    async fn get_health(&self, id: &PluginId) -> Result<HealthStatus>;
233}
234
235/// Plugin configuration
236#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct PluginConfig {
238    /// Enable plugin system
239    pub enabled: bool,
240    /// Plugin directories
241    pub plugin_dirs: Vec<PathBuf>,
242    /// Auto-load plugins on startup
243    pub auto_load: bool,
244    /// Plugin allowlist (if empty, all allowed)
245    pub allowlist: Vec<String>,
246    /// Plugin denylist
247    pub denylist: Vec<String>,
248    /// Enable WASM plugins (planned for future release)
249    // #[cfg(feature = "wasm")]
250    // pub wasm_enabled: bool,
251    /// Maximum plugin execution time
252    pub max_execution_time_ms: u64,
253    /// Plugin isolation level
254    pub isolation_level: IsolationLevel,
255}
256
257impl Default for PluginConfig {
258    fn default() -> Self {
259        Self {
260            enabled: false,
261            plugin_dirs: vec![PathBuf::from("./plugins")],
262            auto_load: true,
263            allowlist: Vec::new(),
264            denylist: Vec::new(),
265            // #[cfg(feature = "wasm")]
266            // wasm_enabled: true,
267            max_execution_time_ms: 5000,
268            isolation_level: IsolationLevel::Standard,
269        }
270    }
271}
272
273/// Plugin isolation level
274#[derive(Debug, Clone, Serialize, Deserialize)]
275#[serde(rename_all = "lowercase")]
276pub enum IsolationLevel {
277    /// No isolation (native plugins)
278    None,
279    /// Standard isolation (separate process)
280    Standard,
281    /// Strong isolation (WASM sandbox)
282    Strong,
283}
284
285/// Plugin handle for active plugins
286pub struct PluginHandle {
287    pub id: PluginId,
288    pub metadata: PluginMetadata,
289    pub plugin: Arc<dyn SecurityPlugin>,
290    pub enabled: bool,
291    pub loaded_at: chrono::DateTime<chrono::Utc>,
292    pub metrics: Arc<tokio::sync::RwLock<PluginMetrics>>,
293}
294
295/// Factory for creating plugin managers
296pub trait PluginManagerFactory: Send + Sync {
297    /// Create a plugin manager
298    fn create(&self, config: &PluginConfig) -> Result<Arc<dyn PluginManagerTrait>>;
299}
300
301/// Default plugin manager factory
302pub struct DefaultPluginManagerFactory;
303
304impl PluginManagerFactory for DefaultPluginManagerFactory {
305    fn create(&self, config: &PluginConfig) -> Result<Arc<dyn PluginManagerTrait>> {
306        if !config.enabled {
307            // Return a no-op manager when plugins are disabled
308            return Ok(Arc::new(NoOpPluginManager));
309        }
310
311        let manager = DefaultPluginManager::new(config.clone())?;
312
313        // Note: Plugin auto-loading happens later, not during factory creation
314        // to avoid runtime-in-runtime issues
315        if config.auto_load {
316            tracing::info!("Plugin auto-loading enabled; plugins will be loaded on first use");
317        }
318
319        Ok(Arc::new(manager))
320    }
321}
322
323/// No-op plugin manager for when plugins are disabled
324struct NoOpPluginManager;
325
326#[async_trait]
327impl PluginManagerTrait for NoOpPluginManager {
328    async fn load_plugin(&self, _path: &Path) -> Result<PluginId> {
329        Err(anyhow::anyhow!("Plugin system disabled"))
330    }
331
332    async fn unload_plugin(&self, _id: &PluginId) -> Result<()> {
333        Ok(())
334    }
335
336    async fn get_plugin(&self, _id: &PluginId) -> Result<PluginInfo> {
337        Err(anyhow::anyhow!("Plugin system disabled"))
338    }
339
340    async fn list_plugins(&self) -> Result<Vec<PluginInfo>> {
341        Ok(Vec::new())
342    }
343
344    async fn scan(&self, _id: &PluginId, _context: ScanContext<'_>) -> Result<Vec<Threat>> {
345        Ok(Vec::new())
346    }
347
348    async fn scan_all(&self, _context: ScanContext<'_>) -> Result<HashMap<PluginId, Vec<Threat>>> {
349        Ok(HashMap::new())
350    }
351
352    async fn reload_plugin(&self, _id: &PluginId) -> Result<()> {
353        Err(anyhow::anyhow!("Plugin system disabled"))
354    }
355
356    async fn get_health(&self, _id: &PluginId) -> Result<HealthStatus> {
357        Err(anyhow::anyhow!("Plugin system disabled"))
358    }
359}