1pub 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#[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#[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
61pub struct Plugin {
63 pub metadata: PluginMetadata,
64 instance: wasmtime::Instance,
65 store: wasmtime::Store<host::HostState>,
66}
67
68impl Plugin {
69 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#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct PluginInput {
78 pub source: String,
79 pub file_path: String,
80 pub language: String,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct PluginOutput {
86 pub findings: Vec<PluginFinding>,
87}
88
89#[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
149pub struct PluginManager {
151 registry: registry::PluginRegistry,
152 engine: wasmtime::Engine,
153}
154
155impl PluginManager {
156 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 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 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 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 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 pub fn analyze(&mut self, source: &str, language: Language) -> Result<Vec<Finding>> {
235 self.registry.analyze_all(source, language)
236 }
237
238 pub fn list_plugins(&self) -> Vec<&PluginMetadata> {
240 self.registry.list()
241 }
242
243 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}