Skip to main content

kindly_guard_server/plugins/
native.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//! Native plugin loader for Rust-based plugins
15//!
16//! Loads plugins compiled as dynamic libraries (.so, .dll, .dylib)
17
18use super::{
19    async_trait, HealthStatus, PluginCapabilities, PluginLoader, PluginMetadata, ScanContext,
20    SecurityPlugin, Severity, Threat, ThreatType,
21};
22use anyhow::Result;
23use std::path::Path;
24use tracing::{debug, info};
25
26/// Native plugin loader
27pub struct NativePluginLoader {
28    _private: (),
29}
30
31impl Default for NativePluginLoader {
32    fn default() -> Self {
33        Self::new()
34    }
35}
36
37impl NativePluginLoader {
38    /// Create a new native plugin loader
39    pub const fn new() -> Self {
40        Self { _private: () }
41    }
42}
43
44#[async_trait]
45impl PluginLoader for NativePluginLoader {
46    async fn load_plugin(&self, path: &Path) -> Result<Box<dyn SecurityPlugin>> {
47        // For now, we'll create example plugins
48        // In a real implementation, this would use libloading to load .so/.dll files
49
50        let filename = path
51            .file_name()
52            .and_then(|n| n.to_str())
53            .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?;
54
55        // Example: Create different plugins based on filename
56        match filename {
57            "sql_injection_plugin.so" | "sql_injection_plugin.dll" => {
58                Ok(Box::new(SqlInjectionPlugin::new()))
59            },
60            "xss_plugin.so" | "xss_plugin.dll" => Ok(Box::new(XssPlugin::new())),
61            "custom_pattern_plugin.so" | "custom_pattern_plugin.dll" => {
62                Ok(Box::new(CustomPatternPlugin::new()))
63            },
64            _ => Err(anyhow::anyhow!("Unknown plugin type: {}", filename)),
65        }
66    }
67
68    async fn validate_plugin(&self, path: &Path) -> Result<PluginMetadata> {
69        // Check if it's a valid plugin file
70        let extension = path
71            .extension()
72            .and_then(|e| e.to_str())
73            .ok_or_else(|| anyhow::anyhow!("No file extension"))?;
74
75        match extension {
76            "so" | "dll" | "dylib" => {
77                // In real implementation, would load and check plugin interface
78                // For now, return mock metadata based on filename
79                let filename = path
80                    .file_stem()
81                    .and_then(|n| n.to_str())
82                    .ok_or_else(|| anyhow::anyhow!("Invalid filename"))?;
83
84                Ok(PluginMetadata {
85                    name: filename.to_string(),
86                    version: "1.0.0".to_string(),
87                    author: "Plugin Author".to_string(),
88                    description: format!("Native plugin: {filename}"),
89                    homepage: None,
90                    threat_types: vec!["custom".to_string()],
91                    capabilities: PluginCapabilities {
92                        scan_text: true,
93                        scan_json: true,
94                        scan_binary: false,
95                        async_scan: true,
96                        batch_scan: false,
97                        max_data_size_mb: Some(10),
98                    },
99                })
100            },
101            _ => Err(anyhow::anyhow!("Not a native plugin file")),
102        }
103    }
104
105    fn loader_type(&self) -> &'static str {
106        "native"
107    }
108}
109
110/// Example SQL injection detection plugin
111struct SqlInjectionPlugin {
112    patterns: Vec<regex::Regex>,
113    config: serde_json::Value,
114}
115
116impl SqlInjectionPlugin {
117    fn new() -> Self {
118        let patterns = vec![
119            regex::Regex::new(r"(?i)(\b(union|select|insert|update|delete|drop|create|alter|exec|execute)\b.*\b(from|where|table|database)\b)").unwrap(),
120            regex::Regex::new(r#"(?i)('|"|;|--|xp_|sp_)"#).unwrap(),
121            regex::Regex::new(r"(?i)(admin'|'or'|'=')").unwrap(),
122        ];
123
124        Self {
125            patterns,
126            config: serde_json::Value::Null,
127        }
128    }
129}
130
131#[async_trait]
132impl SecurityPlugin for SqlInjectionPlugin {
133    fn metadata(&self) -> PluginMetadata {
134        PluginMetadata {
135            name: "SQL Injection Detector".to_string(),
136            version: "1.0.0".to_string(),
137            author: "KindlyGuard Team".to_string(),
138            description: "Detects SQL injection attempts in text and JSON data".to_string(),
139            homepage: Some("https://kindlyguard.dev/plugins/sql-injection".to_string()),
140            threat_types: vec!["sql_injection".to_string()],
141            capabilities: PluginCapabilities {
142                scan_text: true,
143                scan_json: true,
144                scan_binary: false,
145                async_scan: true,
146                batch_scan: false,
147                max_data_size_mb: Some(10),
148            },
149        }
150    }
151
152    async fn initialize(&mut self, config: serde_json::Value) -> Result<()> {
153        self.config = config;
154        info!("SQL Injection plugin initialized");
155        Ok(())
156    }
157
158    async fn scan(&self, context: ScanContext<'_>) -> Result<Vec<Threat>> {
159        let text = String::from_utf8_lossy(context.data);
160        let mut threats = Vec::new();
161
162        for (i, pattern) in self.patterns.iter().enumerate() {
163            if let Some(m) = pattern.find(&text) {
164                threats.push(Threat {
165                    threat_type: ThreatType::SqlInjection,
166                    severity: Severity::High,
167                    location: crate::scanner::Location::Text {
168                        offset: m.start(),
169                        length: m.len(),
170                    },
171                    description: format!("SQL injection pattern {} detected", i + 1),
172                    remediation: Some("Sanitize input and use parameterized queries".to_string()),
173                });
174            }
175        }
176
177        Ok(threats)
178    }
179
180    async fn health_check(&self) -> Result<HealthStatus> {
181        Ok(HealthStatus {
182            healthy: true,
183            message: "SQL Injection plugin is healthy".to_string(),
184            last_check: chrono::Utc::now(),
185            metrics: self.get_metrics(),
186        })
187    }
188
189    async fn shutdown(&mut self) -> Result<()> {
190        info!("SQL Injection plugin shutting down");
191        Ok(())
192    }
193}
194
195/// Example XSS detection plugin
196struct XssPlugin {
197    patterns: Vec<regex::Regex>,
198}
199
200impl XssPlugin {
201    fn new() -> Self {
202        let patterns = vec![
203            regex::Regex::new(r"<script[^>]*>.*?</script>").unwrap(),
204            regex::Regex::new(r"javascript:").unwrap(),
205            regex::Regex::new(r"on\w+\s*=").unwrap(),
206        ];
207
208        Self { patterns }
209    }
210}
211
212#[async_trait]
213impl SecurityPlugin for XssPlugin {
214    fn metadata(&self) -> PluginMetadata {
215        PluginMetadata {
216            name: "XSS Detector".to_string(),
217            version: "1.0.0".to_string(),
218            author: "KindlyGuard Team".to_string(),
219            description: "Detects cross-site scripting attempts".to_string(),
220            homepage: None,
221            threat_types: vec!["xss".to_string()],
222            capabilities: PluginCapabilities {
223                scan_text: true,
224                scan_json: true,
225                scan_binary: false,
226                async_scan: true,
227                batch_scan: false,
228                max_data_size_mb: Some(5),
229            },
230        }
231    }
232
233    async fn initialize(&mut self, _config: serde_json::Value) -> Result<()> {
234        info!("XSS plugin initialized");
235        Ok(())
236    }
237
238    async fn scan(&self, context: ScanContext<'_>) -> Result<Vec<Threat>> {
239        let text = String::from_utf8_lossy(context.data);
240        let mut threats = Vec::new();
241
242        for pattern in &self.patterns {
243            if let Some(m) = pattern.find(&text) {
244                threats.push(Threat {
245                    threat_type: ThreatType::CrossSiteScripting,
246                    severity: Severity::High,
247                    location: crate::scanner::Location::Text {
248                        offset: m.start(),
249                        length: m.len(),
250                    },
251                    description: "XSS pattern detected".to_string(),
252                    remediation: Some("Escape HTML entities and validate input".to_string()),
253                });
254            }
255        }
256
257        Ok(threats)
258    }
259
260    async fn health_check(&self) -> Result<HealthStatus> {
261        Ok(HealthStatus {
262            healthy: true,
263            message: "XSS plugin is healthy".to_string(),
264            last_check: chrono::Utc::now(),
265            metrics: self.get_metrics(),
266        })
267    }
268
269    async fn shutdown(&mut self) -> Result<()> {
270        info!("XSS plugin shutting down");
271        Ok(())
272    }
273}
274
275/// Example custom pattern plugin
276struct CustomPatternPlugin {
277    patterns: Vec<(String, regex::Regex, Severity)>,
278}
279
280impl CustomPatternPlugin {
281    const fn new() -> Self {
282        Self {
283            patterns: Vec::new(),
284        }
285    }
286}
287
288#[async_trait]
289impl SecurityPlugin for CustomPatternPlugin {
290    fn metadata(&self) -> PluginMetadata {
291        PluginMetadata {
292            name: "Custom Pattern Detector".to_string(),
293            version: "1.0.0".to_string(),
294            author: "User".to_string(),
295            description: "Detects custom threat patterns".to_string(),
296            homepage: None,
297            threat_types: vec!["custom".to_string()],
298            capabilities: PluginCapabilities {
299                scan_text: true,
300                scan_json: false,
301                scan_binary: false,
302                async_scan: true,
303                batch_scan: false,
304                max_data_size_mb: Some(1),
305            },
306        }
307    }
308
309    async fn initialize(&mut self, config: serde_json::Value) -> Result<()> {
310        // Load patterns from config
311        if let Some(patterns) = config.get("patterns").and_then(|p| p.as_array()) {
312            for pattern_config in patterns {
313                if let (Some(name), Some(regex), Some(severity)) = (
314                    pattern_config.get("name").and_then(|n| n.as_str()),
315                    pattern_config.get("pattern").and_then(|p| p.as_str()),
316                    pattern_config.get("severity").and_then(|s| s.as_str()),
317                ) {
318                    let severity = match severity {
319                        "low" => Severity::Low,
320                        "medium" => Severity::Medium,
321                        "high" => Severity::High,
322                        "critical" => Severity::Critical,
323                        _ => Severity::Medium,
324                    };
325
326                    if let Ok(re) = regex::Regex::new(regex) {
327                        self.patterns.push((name.to_string(), re, severity));
328                        debug!("Added custom pattern: {}", name);
329                    }
330                }
331            }
332        }
333
334        info!(
335            "Custom pattern plugin initialized with {} patterns",
336            self.patterns.len()
337        );
338        Ok(())
339    }
340
341    async fn scan(&self, context: ScanContext<'_>) -> Result<Vec<Threat>> {
342        let text = String::from_utf8_lossy(context.data);
343        let mut threats = Vec::new();
344
345        for (name, pattern, severity) in &self.patterns {
346            if let Some(m) = pattern.find(&text) {
347                threats.push(Threat {
348                    threat_type: ThreatType::Custom(name.clone()),
349                    severity: *severity,
350                    location: crate::scanner::Location::Text {
351                        offset: m.start(),
352                        length: m.len(),
353                    },
354                    description: format!("Custom pattern '{name}' detected"),
355                    remediation: None,
356                });
357            }
358        }
359
360        Ok(threats)
361    }
362
363    async fn health_check(&self) -> Result<HealthStatus> {
364        Ok(HealthStatus {
365            healthy: true,
366            message: format!(
367                "Custom pattern plugin with {} patterns",
368                self.patterns.len()
369            ),
370            last_check: chrono::Utc::now(),
371            metrics: self.get_metrics(),
372        })
373    }
374
375    async fn shutdown(&mut self) -> Result<()> {
376        info!("Custom pattern plugin shutting down");
377        self.patterns.clear();
378        Ok(())
379    }
380}