rustant_plugins/
security.rs1use crate::PluginMetadata;
6use serde::{Deserialize, Serialize};
7
8#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
10pub enum PluginCapability {
11 ToolRegistration,
13 HookRegistration,
15 FileSystemAccess,
17 NetworkAccess,
19 ShellExecution,
21 SecretAccess,
23}
24
25#[derive(Debug)]
27pub struct SecurityValidationResult {
28 pub is_valid: bool,
29 pub warnings: Vec<String>,
30 pub errors: Vec<String>,
31}
32
33pub struct PluginSecurityValidator {
35 blocked_names: Vec<String>,
37 max_capabilities: Option<usize>,
39}
40
41impl PluginSecurityValidator {
42 pub fn new() -> Self {
44 Self {
45 blocked_names: Vec::new(),
46 max_capabilities: None,
47 }
48 }
49
50 pub fn block_name(&mut self, name: impl Into<String>) {
52 self.blocked_names.push(name.into());
53 }
54
55 pub fn set_max_capabilities(&mut self, max: usize) {
57 self.max_capabilities = Some(max);
58 }
59
60 pub fn validate(&self, metadata: &PluginMetadata) -> SecurityValidationResult {
62 let mut errors = Vec::new();
63 let mut warnings = Vec::new();
64
65 if self.blocked_names.contains(&metadata.name) {
67 errors.push(format!("Plugin '{}' is blocked", metadata.name));
68 }
69
70 if metadata.name.is_empty() {
72 errors.push("Plugin name cannot be empty".into());
73 }
74
75 if metadata.version.is_empty() {
77 errors.push("Plugin version cannot be empty".into());
78 }
79
80 if let Some(max) = self.max_capabilities
82 && metadata.capabilities.len() > max
83 {
84 errors.push(format!(
85 "Plugin requests {} capabilities (max: {})",
86 metadata.capabilities.len(),
87 max
88 ));
89 }
90
91 for cap in &metadata.capabilities {
93 match cap {
94 PluginCapability::ShellExecution => {
95 warnings.push("Plugin requests shell execution capability".into());
96 }
97 PluginCapability::SecretAccess => {
98 warnings.push("Plugin requests secret/credential access".into());
99 }
100 PluginCapability::FileSystemAccess => {
101 warnings.push("Plugin requests filesystem access".into());
102 }
103 PluginCapability::NetworkAccess => {
104 warnings.push("Plugin requests network access".into());
105 }
106 _ => {}
107 }
108 }
109
110 if let Some(ref min_version) = metadata.min_core_version
112 && !is_version_compatible(min_version, env!("CARGO_PKG_VERSION"))
113 {
114 errors.push(format!(
115 "Plugin requires core version >= {} (current: {})",
116 min_version,
117 env!("CARGO_PKG_VERSION")
118 ));
119 }
120
121 let is_valid = errors.is_empty();
122 SecurityValidationResult {
123 is_valid,
124 warnings,
125 errors,
126 }
127 }
128}
129
130impl Default for PluginSecurityValidator {
131 fn default() -> Self {
132 Self::new()
133 }
134}
135
136fn is_version_compatible(required: &str, current: &str) -> bool {
139 let req_parts: Vec<u32> = required.split('.').filter_map(|p| p.parse().ok()).collect();
140 let cur_parts: Vec<u32> = current.split('.').filter_map(|p| p.parse().ok()).collect();
141
142 for i in 0..3 {
143 let req = req_parts.get(i).copied().unwrap_or(0);
144 let cur = cur_parts.get(i).copied().unwrap_or(0);
145 if cur > req {
146 return true;
147 }
148 if cur < req {
149 return false;
150 }
151 }
152 true }
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn make_metadata(name: &str, caps: Vec<PluginCapability>) -> PluginMetadata {
160 PluginMetadata {
161 name: name.into(),
162 version: "1.0.0".into(),
163 description: "Test".into(),
164 author: None,
165 min_core_version: None,
166 capabilities: caps,
167 }
168 }
169
170 #[test]
171 fn test_validate_clean_plugin() {
172 let validator = PluginSecurityValidator::new();
173 let meta = make_metadata("safe-plugin", vec![PluginCapability::ToolRegistration]);
174 let result = validator.validate(&meta);
175 assert!(result.is_valid);
176 assert!(result.errors.is_empty());
177 }
178
179 #[test]
180 fn test_validate_blocked_name() {
181 let mut validator = PluginSecurityValidator::new();
182 validator.block_name("evil-plugin");
183 let meta = make_metadata("evil-plugin", vec![]);
184 let result = validator.validate(&meta);
185 assert!(!result.is_valid);
186 }
187
188 #[test]
189 fn test_validate_empty_name() {
190 let validator = PluginSecurityValidator::new();
191 let meta = make_metadata("", vec![]);
192 let result = validator.validate(&meta);
193 assert!(!result.is_valid);
194 }
195
196 #[test]
197 fn test_validate_dangerous_capabilities_warn() {
198 let validator = PluginSecurityValidator::new();
199 let meta = make_metadata(
200 "risky",
201 vec![
202 PluginCapability::ShellExecution,
203 PluginCapability::SecretAccess,
204 ],
205 );
206 let result = validator.validate(&meta);
207 assert!(result.is_valid); assert_eq!(result.warnings.len(), 2);
209 }
210
211 #[test]
212 fn test_validate_max_capabilities() {
213 let mut validator = PluginSecurityValidator::new();
214 validator.set_max_capabilities(1);
215 let meta = make_metadata(
216 "greedy",
217 vec![
218 PluginCapability::ToolRegistration,
219 PluginCapability::HookRegistration,
220 PluginCapability::NetworkAccess,
221 ],
222 );
223 let result = validator.validate(&meta);
224 assert!(!result.is_valid);
225 }
226
227 #[test]
228 fn test_version_compatible() {
229 assert!(is_version_compatible("0.1.0", "0.1.0"));
230 assert!(is_version_compatible("0.1.0", "0.2.0"));
231 assert!(is_version_compatible("0.1.0", "1.0.0"));
232 assert!(!is_version_compatible("1.0.0", "0.9.0"));
233 assert!(!is_version_compatible("0.2.0", "0.1.9"));
234 }
235
236 #[test]
237 fn test_version_incompatible_core() {
238 let validator = PluginSecurityValidator::new();
239 let mut meta = make_metadata("new-plugin", vec![]);
240 meta.min_core_version = Some("999.0.0".into());
241 let result = validator.validate(&meta);
242 assert!(!result.is_valid);
243 }
244
245 #[test]
246 fn test_capability_serialization() {
247 let cap = PluginCapability::ShellExecution;
248 let json = serde_json::to_string(&cap).unwrap();
249 let restored: PluginCapability = serde_json::from_str(&json).unwrap();
250 assert_eq!(restored, PluginCapability::ShellExecution);
251 }
252}