Skip to main content

rma_plugins/
lib.rs

1//! WASM Plugin System for Qryon
2//!
3//! This crate provides a WebAssembly-based plugin system that allows users
4//! to write custom analysis rules in any language that compiles to WASM.
5//!
6//! # Plugin Interface
7//!
8//! Plugins implement a simple interface:
9//! - `analyze(source: &str, language: &str) -> Vec<Finding>`
10//!
11//! # Example Plugin (Rust compiled to WASM)
12//!
13//! ```ignore
14//! #[no_mangle]
15//! pub extern "C" fn analyze(source_ptr: *const u8, source_len: usize) -> *mut Finding {
16//!     // ... analysis logic
17//! }
18//! ```
19
20pub mod host;
21pub mod loader;
22pub mod registry;
23
24use anyhow::Result;
25use rma_common::{Finding, Language};
26use serde::{Deserialize, Serialize};
27use std::path::Path;
28use thiserror::Error;
29use tracing::{debug, info, warn};
30
31/// Errors that can occur in the plugin system
32#[derive(Error, Debug)]
33pub enum PluginError {
34    #[error("Failed to load plugin: {0}")]
35    LoadError(String),
36
37    #[error("Plugin execution failed: {0}")]
38    ExecutionError(String),
39
40    #[error("Invalid plugin interface: {0}")]
41    InterfaceError(String),
42
43    #[error("Plugin not found: {0}")]
44    NotFound(String),
45
46    #[error("WASM error: {0}")]
47    WasmError(#[from] anyhow::Error),
48}
49
50/// Metadata about a loaded plugin
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct PluginMetadata {
53    pub name: String,
54    pub version: String,
55    pub description: String,
56    pub author: Option<String>,
57    pub languages: Vec<Language>,
58    pub rules: Vec<String>,
59}
60
61/// A loaded WASM plugin
62pub struct Plugin {
63    pub metadata: PluginMetadata,
64    instance: wasmtime::Instance,
65    store: wasmtime::Store<host::HostState>,
66}
67
68impl Plugin {
69    /// Run the plugin's analysis on the given source code
70    pub fn analyze(&mut self, source: &str, language: Language) -> Result<Vec<Finding>> {
71        host::call_analyze(&mut self.store, &self.instance, source, language)
72    }
73}
74
75/// Input data passed to plugin analysis functions
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct PluginInput {
78    pub source: String,
79    pub file_path: String,
80    pub language: String,
81}
82
83/// Output from plugin analysis
84#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PluginOutput {
86    pub findings: Vec<PluginFinding>,
87}
88
89/// A finding reported by a plugin
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct PluginFinding {
92    pub rule_id: String,
93    pub message: String,
94    pub severity: String,
95    pub start_line: usize,
96    pub start_column: usize,
97    pub end_line: usize,
98    pub end_column: usize,
99    pub snippet: Option<String>,
100    pub suggestion: Option<String>,
101}
102
103impl From<PluginFinding> for Finding {
104    fn from(pf: PluginFinding) -> Self {
105        let mut finding = Finding {
106            id: format!(
107                "plugin-{}-{}-{}",
108                pf.rule_id, pf.start_line, pf.start_column
109            ),
110            rule_id: pf.rule_id,
111            message: pf.message,
112            severity: match pf.severity.to_lowercase().as_str() {
113                "critical" => rma_common::Severity::Critical,
114                "error" => rma_common::Severity::Error,
115                "warning" => rma_common::Severity::Warning,
116                _ => rma_common::Severity::Info,
117            },
118            location: rma_common::SourceLocation::new(
119                std::path::PathBuf::new(),
120                pf.start_line,
121                pf.start_column,
122                pf.end_line,
123                pf.end_column,
124            ),
125            language: Language::Unknown,
126            snippet: pf.snippet,
127            suggestion: pf.suggestion,
128            fix: None,
129            confidence: rma_common::Confidence::Medium,
130            category: rma_common::FindingCategory::Quality,
131            subcategory: None,
132            technology: None,
133            impact: None,
134            likelihood: None,
135            source: rma_common::FindingSource::Plugin,
136            fingerprint: None,
137            properties: None,
138            occurrence_count: None,
139            additional_locations: None,
140            ai_verdict: None,
141            ai_explanation: None,
142            ai_confidence: None,
143        };
144        finding.compute_fingerprint();
145        finding
146    }
147}
148
149/// The main plugin manager
150pub struct PluginManager {
151    registry: registry::PluginRegistry,
152    engine: wasmtime::Engine,
153}
154
155impl PluginManager {
156    /// Create a new plugin manager
157    pub fn new() -> Result<Self> {
158        let mut config = wasmtime::Config::new();
159        config.wasm_component_model(true);
160        config.async_support(false);
161
162        let engine = wasmtime::Engine::new(&config)?;
163
164        Ok(Self {
165            registry: registry::PluginRegistry::new(),
166            engine,
167        })
168    }
169
170    /// Load a plugin from a WASM file
171    pub fn load_plugin(&mut self, path: &Path) -> Result<String, PluginError> {
172        info!("Loading plugin from {:?}", path);
173
174        let wasm_bytes = std::fs::read(path)
175            .map_err(|e| PluginError::LoadError(format!("Failed to read file: {}", e)))?;
176
177        let module = wasmtime::Module::new(&self.engine, &wasm_bytes)
178            .map_err(|e| PluginError::LoadError(format!("Failed to compile WASM: {}", e)))?;
179
180        let mut store = wasmtime::Store::new(&self.engine, host::HostState::new());
181
182        // Create linker with host functions
183        let linker = host::create_linker(&self.engine)?;
184
185        let instance = linker
186            .instantiate(&mut store, &module)
187            .map_err(|e| PluginError::LoadError(format!("Failed to instantiate: {}", e)))?;
188
189        // Get plugin metadata
190        let metadata = host::get_plugin_metadata(&mut store, &instance)?;
191        let plugin_name = metadata.name.clone();
192
193        let plugin = Plugin {
194            metadata,
195            instance,
196            store,
197        };
198
199        self.registry.register(plugin)?;
200
201        Ok(plugin_name)
202    }
203
204    /// Load all plugins from a directory
205    pub fn load_plugins_from_dir(&mut self, dir: &Path) -> Result<Vec<String>> {
206        let mut loaded = Vec::new();
207
208        if !dir.exists() {
209            debug!("Plugin directory {:?} does not exist", dir);
210            return Ok(loaded);
211        }
212
213        for entry in std::fs::read_dir(dir)? {
214            let entry = entry?;
215            let path = entry.path();
216
217            if path.extension().map(|e| e == "wasm").unwrap_or(false) {
218                match self.load_plugin(&path) {
219                    Ok(name) => {
220                        info!("Loaded plugin: {}", name);
221                        loaded.push(name);
222                    }
223                    Err(e) => {
224                        warn!("Failed to load plugin {:?}: {}", path, e);
225                    }
226                }
227            }
228        }
229
230        Ok(loaded)
231    }
232
233    /// Run all applicable plugins on the given source
234    pub fn analyze(&mut self, source: &str, language: Language) -> Result<Vec<Finding>> {
235        self.registry.analyze_all(source, language)
236    }
237
238    /// List all loaded plugins
239    pub fn list_plugins(&self) -> Vec<&PluginMetadata> {
240        self.registry.list()
241    }
242
243    /// Unload a plugin by name
244    pub fn unload_plugin(&mut self, name: &str) -> Result<(), PluginError> {
245        self.registry.unregister(name)
246    }
247}
248
249impl Default for PluginManager {
250    fn default() -> Self {
251        Self::new().expect("Failed to create plugin manager")
252    }
253}
254
255#[cfg(test)]
256mod tests {
257    use super::*;
258
259    #[test]
260    fn test_plugin_manager_creation() {
261        let manager = PluginManager::new();
262        assert!(manager.is_ok());
263    }
264
265    #[test]
266    fn test_plugin_finding_conversion() {
267        let pf = PluginFinding {
268            rule_id: "test-rule".to_string(),
269            message: "Test message".to_string(),
270            severity: "warning".to_string(),
271            start_line: 10,
272            start_column: 5,
273            end_line: 10,
274            end_column: 15,
275            snippet: Some("test code".to_string()),
276            suggestion: None,
277        };
278
279        let finding: Finding = pf.into();
280        assert_eq!(finding.rule_id, "test-rule");
281        assert_eq!(finding.severity, rma_common::Severity::Warning);
282    }
283}