1use super::*;
10use std::collections::HashSet;
11use std::path::Path;
12
13use mockforge_plugin_core::{
15 FilesystemPermissions, NetworkPermissions, PluginCapabilities, PluginId, PluginManifest,
16 ResourceLimits,
17};
18
19use wasmparser::{Parser, Payload};
21
22use ring::signature;
24
25use shellexpand;
27
28use base64::{engine::general_purpose, Engine as _};
30use hex;
31
32use serde_json;
34
35#[derive(Debug, Clone)]
37struct PluginSignature {
38 algorithm: String,
39 signature: Vec<u8>,
40 key_id: String,
41}
42
43pub struct PluginValidator {
45 config: PluginLoaderConfig,
47 security_policies: SecurityPolicies,
49}
50
51impl PluginValidator {
52 pub fn new(config: PluginLoaderConfig) -> Self {
54 Self {
55 config,
56 security_policies: SecurityPolicies::default(),
57 }
58 }
59
60 pub async fn validate_manifest(&self, manifest: &PluginManifest) -> LoaderResult<()> {
62 let mut errors = Vec::new();
63
64 if let Err(validation_error) = manifest.validate() {
66 errors.push(PluginLoaderError::manifest(validation_error));
67 }
68
69 if let Err(e) = self.security_policies.validate_manifest(manifest) {
71 errors.push(e);
72 }
73
74 let mut visited = std::collections::HashSet::new();
76 visited.insert(manifest.info.id.clone());
77 if let Err(e) = self
78 .validate_dependencies(&manifest.info.id, &manifest.dependencies, &mut visited)
79 .await
80 {
81 errors.push(e);
82 }
83
84 if errors.is_empty() {
85 Ok(())
86 } else {
87 Err(PluginLoaderError::validation(format!(
88 "Manifest validation failed with {} errors: {}",
89 errors.len(),
90 errors.into_iter().map(|e| e.to_string()).collect::<Vec<_>>().join(", ")
91 )))
92 }
93 }
94
95 pub fn validate_capabilities(&self, capability_names: &[String]) -> LoaderResult<()> {
97 let capabilities = PluginCapabilities {
99 network: NetworkPermissions::default(),
100 filesystem: FilesystemPermissions::default(),
101 resources: ResourceLimits::default(),
102 custom: capability_names
103 .iter()
104 .map(|name| (name.clone(), serde_json::Value::Bool(true)))
105 .collect(),
106 };
107 self.security_policies.validate_capabilities(&capabilities)
108 }
109
110 pub async fn validate_wasm_file(&self, wasm_path: &Path) -> LoaderResult<()> {
112 if !wasm_path.exists() {
114 return Err(PluginLoaderError::fs("WASM file does not exist".to_string()));
115 }
116
117 let metadata = tokio::fs::metadata(wasm_path)
118 .await
119 .map_err(|e| PluginLoaderError::fs(format!("Cannot read WASM file metadata: {}", e)))?;
120
121 if !metadata.is_file() {
122 return Err(PluginLoaderError::fs("WASM path is not a file".to_string()));
123 }
124
125 let file_size = metadata.len();
127 if file_size > self.security_policies.max_wasm_file_size {
128 return Err(PluginLoaderError::security(format!(
129 "WASM file too large: {} bytes (max: {} bytes)",
130 file_size, self.security_policies.max_wasm_file_size
131 )));
132 }
133
134 self.validate_wasm_module(wasm_path).await?;
136
137 Ok(())
138 }
139
140 pub async fn validate_plugin_file(&self, plugin_path: &Path) -> LoaderResult<PluginManifest> {
142 if !plugin_path.exists() {
143 return Err(PluginLoaderError::fs("Plugin path does not exist".to_string()));
144 }
145
146 if !plugin_path.is_dir() {
147 return Err(PluginLoaderError::fs("Plugin path must be a directory".to_string()));
148 }
149
150 let manifest_path = plugin_path.join("plugin.yaml");
152 if !manifest_path.exists() {
153 return Err(PluginLoaderError::manifest("plugin.yaml not found".to_string()));
154 }
155
156 let manifest = PluginManifest::from_file(&manifest_path)
158 .map_err(|e| PluginLoaderError::manifest(format!("Failed to load manifest: {}", e)))?;
159
160 self.validate_manifest(&manifest).await?;
162
163 let wasm_files: Vec<_> = std::fs::read_dir(plugin_path)
165 .map_err(|e| PluginLoaderError::fs(format!("Cannot read plugin directory: {}", e)))?
166 .filter_map(|entry| entry.ok())
167 .map(|entry| entry.path())
168 .filter(|path| path.extension().is_some_and(|ext| ext == "wasm"))
169 .collect();
170
171 if wasm_files.is_empty() {
172 return Err(PluginLoaderError::load(
173 "No WebAssembly file found in plugin directory".to_string(),
174 ));
175 }
176
177 if wasm_files.len() > 1 {
178 return Err(PluginLoaderError::load(
179 "Multiple WebAssembly files found in plugin directory".to_string(),
180 ));
181 }
182
183 if !self.config.skip_wasm_validation {
185 self.validate_wasm_file(&wasm_files[0]).await?;
186 }
187
188 Ok(manifest)
189 }
190
191 async fn validate_dependencies(
193 &self,
194 current_plugin_id: &PluginId,
195 dependencies: &std::collections::HashMap<
196 mockforge_plugin_core::PluginId,
197 mockforge_plugin_core::PluginVersion,
198 >,
199 visited: &mut std::collections::HashSet<PluginId>,
200 ) -> LoaderResult<()> {
201 for (plugin_id, version) in dependencies {
202 if self.would_create_circular_dependency(current_plugin_id, plugin_id, visited) {
204 return Err(PluginLoaderError::ValidationError {
205 message: format!(
206 "Circular dependency detected: '{}' -> '{}'",
207 current_plugin_id.0, plugin_id.0
208 ),
209 });
210 }
211
212 if plugin_id.0.is_empty() {
214 return Err(PluginLoaderError::ValidationError {
215 message: "Dependency plugin ID cannot be empty".to_string(),
216 });
217 }
218
219 if plugin_id.0.len() > 100 {
220 return Err(PluginLoaderError::ValidationError {
221 message: format!(
222 "Dependency plugin ID '{}' is too long (max 100 characters)",
223 plugin_id.0
224 ),
225 });
226 }
227
228 if version.major == 0 && version.minor == 0 && version.patch == 0 {
230 tracing::warn!("Dependency '{}' specifies version 0.0.0 which may indicate development/testing", plugin_id.0);
231 }
232
233 if plugin_id.0.contains("..") || plugin_id.0.contains("/") || plugin_id.0.contains("\\")
235 {
236 return Err(PluginLoaderError::SecurityViolation {
237 violation: format!(
238 "Dependency plugin ID '{}' contains potentially unsafe characters",
239 plugin_id.0
240 ),
241 });
242 }
243
244 }
250
251 Ok(())
252 }
253
254 fn would_create_circular_dependency(
258 &self,
259 current_plugin_id: &PluginId,
260 dependency_id: &PluginId,
261 visited: &mut std::collections::HashSet<PluginId>,
262 ) -> bool {
263 if dependency_id == current_plugin_id {
265 return true;
266 }
267
268 visited.contains(dependency_id)
274 }
275
276 async fn validate_wasm_module(&self, wasm_path: &Path) -> LoaderResult<()> {
278 let module = wasmtime::Module::from_file(&wasmtime::Engine::default(), wasm_path)
280 .map_err(|e| PluginLoaderError::wasm(format!("Invalid WASM module: {}", e)))?;
281
282 self.security_policies.validate_wasm_module(&module)?;
284
285 Ok(())
286 }
287
288 pub async fn validate_plugin_signature(
290 &self,
291 plugin_path: &Path,
292 manifest: &PluginManifest,
293 ) -> LoaderResult<()> {
294 if self.config.allow_unsigned {
296 return Ok(());
297 }
298
299 let sig_path = plugin_path.with_extension("sig");
301 if !sig_path.exists() {
302 return Err(PluginLoaderError::SecurityViolation {
303 violation: format!(
304 "Plugin '{}' requires a signature but none was found",
305 manifest.info.id.0
306 ),
307 });
308 }
309
310 let signature_data =
312 std::fs::read(&sig_path).map_err(|e| PluginLoaderError::ValidationError {
313 message: format!("Failed to read signature file: {}", e),
314 })?;
315
316 let signature = self.parse_signature(&signature_data)?;
318
319 let plugin_data =
321 std::fs::read(plugin_path).map_err(|e| PluginLoaderError::ValidationError {
322 message: format!("Failed to read plugin file: {}", e),
323 })?;
324
325 self.verify_signature(&plugin_data, &signature, manifest).await?;
327
328 Ok(())
329 }
330
331 fn parse_signature(&self, data: &[u8]) -> Result<PluginSignature, PluginLoaderError> {
333 let sig_json: serde_json::Value =
337 serde_json::from_slice(data).map_err(|e| PluginLoaderError::ValidationError {
338 message: format!("Invalid signature JSON format: {}", e),
339 })?;
340
341 let algorithm = sig_json
342 .get("algorithm")
343 .and_then(|v| v.as_str())
344 .ok_or_else(|| PluginLoaderError::ValidationError {
345 message: "Missing or invalid 'algorithm' field".to_string(),
346 })?
347 .to_string();
348
349 let signature_hex =
350 sig_json.get("signature").and_then(|v| v.as_str()).ok_or_else(|| {
351 PluginLoaderError::ValidationError {
352 message: "Missing or invalid 'signature' field".to_string(),
353 }
354 })?;
355
356 let key_id = sig_json
357 .get("key_id")
358 .and_then(|v| v.as_str())
359 .ok_or_else(|| PluginLoaderError::ValidationError {
360 message: "Missing or invalid 'key_id' field".to_string(),
361 })?
362 .to_string();
363
364 if !["rsa", "ecdsa", "ed25519"].contains(&algorithm.as_str()) {
366 return Err(PluginLoaderError::ValidationError {
367 message: format!("Unsupported signature algorithm: {}", algorithm),
368 });
369 }
370
371 let signature =
373 hex::decode(signature_hex).map_err(|e| PluginLoaderError::ValidationError {
374 message: format!("Invalid signature hex: {}", e),
375 })?;
376
377 Ok(PluginSignature {
378 algorithm,
379 signature,
380 key_id,
381 })
382 }
383
384 async fn verify_signature(
386 &self,
387 data: &[u8],
388 signature: &PluginSignature,
389 manifest: &PluginManifest,
390 ) -> LoaderResult<()> {
391 let public_key = self.get_trusted_key(&signature.key_id)?;
393
394 match signature.algorithm.as_str() {
396 "rsa" => {
397 self.verify_rsa_signature(data, &signature.signature, &public_key)?;
399 }
400 "ecdsa" => {
401 self.verify_ecdsa_signature(data, &signature.signature, &public_key)?;
403 }
404 "ed25519" => {
405 self.verify_ed25519_signature(data, &signature.signature, &public_key)?;
407 }
408 _ => {
409 return Err(PluginLoaderError::ValidationError {
410 message: format!("Unsupported algorithm: {}", signature.algorithm),
411 });
412 }
413 }
414
415 self.validate_key_authorization(&signature.key_id, manifest)?;
417
418 Ok(())
419 }
420
421 fn get_trusted_key(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
423 if !self.config.trusted_keys.contains(&key_id.to_string()) {
425 return Err(PluginLoaderError::SecurityViolation {
426 violation: format!("Key '{}' is not in the trusted keys list", key_id),
427 });
428 }
429
430 self.load_key_from_store(key_id)
433 }
434
435 fn load_key_from_store(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
437 if let Ok(key_data) = self.load_key_from_env(key_id) {
439 tracing::info!("Loaded key '{}' from environment variable", key_id);
440 return Ok(key_data);
441 }
442
443 if let Some(key_data) = self.config.key_data.get(key_id) {
445 tracing::info!("Loaded key '{}' from configuration", key_id);
446 return Ok(key_data.clone());
447 }
448
449 if let Ok(key_data) = self.load_key_from_filesystem(key_id) {
451 tracing::info!("Loaded key '{}' from filesystem", key_id);
452 return Ok(key_data);
453 }
454
455 Err(PluginLoaderError::SecurityViolation {
469 violation: format!("Could not find key data for trusted key: {}", key_id),
470 })
471 }
472
473 fn load_key_from_env(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
475 let b64_env_key = format!("MOCKFORGE_KEY_{}_B64", key_id.to_uppercase().replace("-", "_"));
477 if let Ok(b64_value) = std::env::var(&b64_env_key) {
478 match general_purpose::STANDARD.decode(&b64_value) {
479 Ok(key_data) => return Ok(key_data),
480 Err(e) => {
481 tracing::warn!("Failed to decode base64 key from {}: {}", b64_env_key, e);
482 }
483 }
484 }
485
486 let hex_env_key = format!("MOCKFORGE_KEY_{}_HEX", key_id.to_uppercase().replace("-", "_"));
488 if let Ok(hex_value) = std::env::var(&hex_env_key) {
489 match hex::decode(&hex_value) {
490 Ok(key_data) => return Ok(key_data),
491 Err(e) => {
492 tracing::warn!("Failed to decode hex key from {}: {}", hex_env_key, e);
493 }
494 }
495 }
496
497 let raw_env_key = format!("MOCKFORGE_KEY_{}", key_id.to_uppercase().replace("-", "_"));
499 if let Ok(key_data) = std::env::var(&raw_env_key) {
500 return Ok(key_data.into_bytes());
501 }
502
503 Err(PluginLoaderError::SecurityViolation {
504 violation: format!("Key not found in environment: {}", key_id),
505 })
506 }
507
508 fn load_key_from_filesystem(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
510 let key_paths = vec![
511 format!("~/.mockforge/keys/{}.der", key_id),
512 format!("~/.mockforge/keys/{}.pem", key_id),
513 format!("/etc/mockforge/keys/{}.der", key_id),
514 format!("/etc/mockforge/keys/{}.pem", key_id),
515 ];
516
517 for key_path in key_paths {
518 let expanded_path = shellexpand::tilde(&key_path);
519 let path = std::path::Path::new(expanded_path.as_ref());
520
521 if path.exists() {
522 match std::fs::read(path) {
523 Ok(key_data) => return Ok(key_data),
524 Err(e) => {
525 tracing::warn!("Failed to read key file {}: {}", path.display(), e);
526 continue;
527 }
528 }
529 }
530 }
531
532 Err(PluginLoaderError::SecurityViolation {
533 violation: format!("Key not found in filesystem: {}", key_id),
534 })
535 }
536
537 #[allow(unused)]
543 fn load_key_from_database(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
544 let db_type = std::env::var("MOCKFORGE_DB_TYPE").map_err(|_| {
545 PluginLoaderError::SecurityViolation {
546 violation: "Database key loading requires MOCKFORGE_DB_TYPE environment variable"
547 .to_string(),
548 }
549 })?;
550
551 let connection_string = std::env::var("MOCKFORGE_DB_CONNECTION").map_err(|_| {
552 PluginLoaderError::SecurityViolation {
553 violation:
554 "Database key loading requires MOCKFORGE_DB_CONNECTION environment variable"
555 .to_string(),
556 }
557 })?;
558
559 let table_name =
560 std::env::var("MOCKFORGE_DB_KEY_TABLE").unwrap_or_else(|_| "plugin_keys".to_string());
561
562 tracing::info!("Database key loading configured: type={}, table={}", db_type, table_name);
563 tracing::debug!("Looking up key '{}' in database", key_id);
564
565 Err(PluginLoaderError::SecurityViolation {
573 violation: format!("Database key loading not fully implemented for key '{}'", key_id),
574 })
575 }
576
577 #[allow(unused)]
584 fn load_key_from_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
585 let kms_provider = std::env::var("MOCKFORGE_KMS_PROVIDER").map_err(|_| {
586 PluginLoaderError::SecurityViolation {
587 violation: "KMS key loading requires MOCKFORGE_KMS_PROVIDER environment variable"
588 .to_string(),
589 }
590 })?;
591
592 match kms_provider.to_lowercase().as_str() {
593 "aws" => self.load_key_from_aws_kms(key_id),
594 "vault" => self.load_key_from_vault(key_id),
595 "azure" => self.load_key_from_azure_kv(key_id),
596 "gcp" => self.load_key_from_gcp_kms(key_id),
597 _ => Err(PluginLoaderError::SecurityViolation {
598 violation: format!("Unsupported KMS provider: {}", kms_provider),
599 }),
600 }
601 }
602
603 fn load_key_from_aws_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
605 let region =
606 std::env::var("MOCKFORGE_KMS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
607
608 tracing::info!("AWS KMS key loading configured: region={}", region);
609 tracing::debug!("Looking up key '{}' in AWS KMS", key_id);
610
611 Err(PluginLoaderError::SecurityViolation {
617 violation: format!("AWS KMS key loading not fully implemented for key '{}'", key_id),
618 })
619 }
620
621 fn load_key_from_vault(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
623 let vault_addr = std::env::var("MOCKFORGE_VAULT_ADDR").map_err(|_| {
624 PluginLoaderError::SecurityViolation {
625 violation: "Vault key loading requires MOCKFORGE_VAULT_ADDR environment variable"
626 .to_string(),
627 }
628 })?;
629
630 let _vault_token = std::env::var("MOCKFORGE_VAULT_TOKEN").map_err(|_| {
631 PluginLoaderError::SecurityViolation {
632 violation: "Vault key loading requires MOCKFORGE_VAULT_TOKEN environment variable"
633 .to_string(),
634 }
635 })?;
636
637 tracing::info!("HashCorp Vault key loading configured: addr={}", vault_addr);
638 tracing::debug!("Looking up key '{}' in Vault", key_id);
639
640 Err(PluginLoaderError::SecurityViolation {
646 violation: format!("Vault key loading not fully implemented for key '{}'", key_id),
647 })
648 }
649
650 fn load_key_from_azure_kv(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
652 tracing::info!("Azure Key Vault key loading requested");
653 tracing::debug!("Looking up key '{}' in Azure Key Vault", key_id);
654
655 Err(PluginLoaderError::SecurityViolation {
661 violation: format!(
662 "Azure Key Vault loading not fully implemented for key '{}'",
663 key_id
664 ),
665 })
666 }
667
668 fn load_key_from_gcp_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
670 tracing::info!("Google Cloud KMS key loading requested");
671 tracing::debug!("Looking up key '{}' in GCP KMS", key_id);
672
673 Err(PluginLoaderError::SecurityViolation {
679 violation: format!(
680 "Google Cloud KMS loading not fully implemented for key '{}'",
681 key_id
682 ),
683 })
684 }
685
686 fn verify_rsa_signature(
688 &self,
689 data: &[u8],
690 signature: &[u8],
691 public_key: &[u8],
692 ) -> LoaderResult<()> {
693 let public_key =
695 signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, public_key);
696
697 public_key
699 .verify(data, signature)
700 .map_err(|e| PluginLoaderError::SecurityViolation {
701 violation: format!("RSA signature verification failed: {}", e),
702 })?;
703
704 Ok(())
705 }
706
707 fn verify_ecdsa_signature(
709 &self,
710 data: &[u8],
711 signature: &[u8],
712 public_key: &[u8],
713 ) -> LoaderResult<()> {
714 let public_key =
716 signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, public_key);
717
718 public_key
720 .verify(data, signature)
721 .map_err(|e| PluginLoaderError::SecurityViolation {
722 violation: format!("ECDSA signature verification failed: {}", e),
723 })?;
724
725 Ok(())
726 }
727
728 fn verify_ed25519_signature(
730 &self,
731 data: &[u8],
732 signature: &[u8],
733 public_key: &[u8],
734 ) -> LoaderResult<()> {
735 let public_key = signature::UnparsedPublicKey::new(&signature::ED25519, public_key);
737
738 public_key
740 .verify(data, signature)
741 .map_err(|e| PluginLoaderError::SecurityViolation {
742 violation: format!("Ed25519 signature verification failed: {}", e),
743 })?;
744
745 Ok(())
746 }
747
748 fn validate_key_authorization(
750 &self,
751 key_id: &str,
752 manifest: &PluginManifest,
753 ) -> LoaderResult<()> {
754 if self.config.trusted_keys.contains(&key_id.to_string()) {
756 return Ok(());
757 }
758
759 Err(PluginLoaderError::SecurityViolation {
760 violation: format!(
761 "Key '{}' is not authorized to sign plugins from '{}'",
762 key_id, manifest.info.author.name
763 ),
764 })
765 }
766
767 pub async fn get_validation_summary(&self, plugin_path: &Path) -> ValidationSummary {
769 let mut summary = ValidationSummary::default();
770
771 if !plugin_path.exists() {
773 summary.errors.push("Plugin path does not exist".to_string());
774 return summary;
775 }
776
777 let manifest_result = self.validate_plugin_file(plugin_path).await;
779 match manifest_result {
780 Ok(manifest) => {
781 summary.manifest_valid = true;
782 summary.manifest = Some(manifest);
783 }
784 Err(e) => {
785 summary.errors.push(format!("Manifest validation failed: {}", e));
786 }
787 }
788
789 if let Ok(wasm_path) = self.find_wasm_file(plugin_path) {
791 let wasm_result = self.validate_wasm_file(&wasm_path).await;
792 summary.wasm_valid = wasm_result.is_ok();
793 if let Err(e) = wasm_result {
794 summary.errors.push(format!("WASM validation failed: {}", e));
795 }
796 } else {
797 summary.errors.push("No WebAssembly file found".to_string());
798 }
799
800 summary.is_valid =
801 summary.manifest_valid && summary.wasm_valid && summary.errors.is_empty();
802 summary
803 }
804
805 fn find_wasm_file(&self, plugin_path: &Path) -> LoaderResult<PathBuf> {
807 let entries = std::fs::read_dir(plugin_path)
808 .map_err(|e| PluginLoaderError::fs(format!("Cannot read directory: {}", e)))?;
809
810 for entry in entries {
811 let entry =
812 entry.map_err(|e| PluginLoaderError::fs(format!("Cannot read entry: {}", e)))?;
813 let path = entry.path();
814
815 if let Some(extension) = path.extension() {
816 if extension == "wasm" {
817 return Ok(path);
818 }
819 }
820 }
821
822 Err(PluginLoaderError::load("No WebAssembly file found".to_string()))
823 }
824}
825
826#[derive(Debug, Clone)]
828pub struct SecurityPolicies {
829 pub max_wasm_file_size: u64,
831 pub allowed_imports: HashSet<String>,
833 pub forbidden_imports: HashSet<String>,
835 pub max_memory_pages: u32,
837 pub max_functions: u32,
839 pub allow_floats: bool,
841 pub allow_simd: bool,
843 pub allow_network_access: bool,
845 pub allow_filesystem_read: bool,
847 pub allow_filesystem_write: bool,
849}
850
851impl Default for SecurityPolicies {
852 fn default() -> Self {
853 let mut allowed_imports = HashSet::new();
854 allowed_imports.insert("env".to_string());
855 allowed_imports.insert("wasi_snapshot_preview1".to_string());
856
857 let mut forbidden_imports = HashSet::new();
858 forbidden_imports.insert("abort".to_string());
859 forbidden_imports.insert("exit".to_string());
860
861 Self {
862 max_wasm_file_size: 10 * 1024 * 1024, allowed_imports,
864 forbidden_imports,
865 max_memory_pages: 256, max_functions: 1000,
867 allow_floats: true,
868 allow_simd: false,
869 allow_network_access: false,
870 allow_filesystem_read: false,
871 allow_filesystem_write: false,
872 }
873 }
874}
875
876impl SecurityPolicies {
877 pub fn validate_manifest(&self, manifest: &PluginManifest) -> LoaderResult<()> {
879 let _caps = PluginCapabilities::from_strings(&manifest.capabilities);
885
886 Ok(())
890 }
891
892 pub fn validate_capabilities(&self, capabilities: &PluginCapabilities) -> LoaderResult<()> {
894 if capabilities.resources.max_memory_bytes > self.max_memory_bytes() {
896 return Err(PluginLoaderError::security(format!(
897 "Memory limit {} exceeds maximum allowed {}",
898 capabilities.resources.max_memory_bytes,
899 self.max_memory_bytes()
900 )));
901 }
902
903 if capabilities.resources.max_cpu_percent > self.max_cpu_percent() {
904 return Err(PluginLoaderError::security(format!(
905 "CPU limit {:.2}% exceeds maximum allowed {:.2}%",
906 capabilities.resources.max_cpu_percent,
907 self.max_cpu_percent()
908 )));
909 }
910
911 Ok(())
912 }
913
914 pub fn validate_wasm_module(&self, module: &wasmtime::Module) -> LoaderResult<()> {
916 self.validate_imports(module)?;
920
921 self.validate_exports(module)?;
923
924 self.validate_memory_usage(module)?;
926
927 self.check_dangerous_operations(module)?;
929
930 self.validate_function_limits(module)?;
932
933 self.validate_data_segments(module)?;
935
936 Ok(())
937 }
938
939 fn validate_imports(&self, module: &wasmtime::Module) -> LoaderResult<()> {
941 let _module_info = module.resources_required();
943
944 for import in module.imports() {
946 let module_name = import.module();
947 let field_name = import.name();
948
949 let allowed_modules = [
951 "wasi_snapshot_preview1",
952 "wasi:io/streams",
953 "wasi:filesystem/types",
954 "mockforge:plugin/host",
955 ];
956
957 if !allowed_modules.contains(&module_name) {
958 return Err(PluginLoaderError::SecurityViolation {
959 violation: format!("Disallowed import module: {}", module_name),
960 });
961 }
962
963 match module_name {
965 "wasi_snapshot_preview1" => {
966 self.validate_wasi_import(field_name)?;
967 }
968 "mockforge:plugin/host" => {
969 self.validate_host_import(field_name)?;
970 }
971 _ => {
972 }
974 }
975 }
976
977 Ok(())
978 }
979
980 fn validate_wasi_import(&self, field_name: &str) -> LoaderResult<()> {
982 let allowed_functions = [
984 "fd_read",
986 "fd_write",
987 "fd_close",
988 "fd_fdstat_get",
989 "path_open",
991 "path_readlink",
992 "path_filestat_get",
993 "clock_time_get",
995 "proc_exit",
997 "random_get",
999 ];
1000
1001 if !allowed_functions.contains(&field_name) {
1002 return Err(PluginLoaderError::SecurityViolation {
1003 violation: format!("Disallowed WASI function: {}", field_name),
1004 });
1005 }
1006
1007 Ok(())
1008 }
1009
1010 fn validate_host_import(&self, field_name: &str) -> LoaderResult<()> {
1012 let allowed_functions = [
1014 "log_message",
1015 "get_config_value",
1016 "store_data",
1017 "retrieve_data",
1018 ];
1019
1020 if !allowed_functions.contains(&field_name) {
1021 return Err(PluginLoaderError::SecurityViolation {
1022 violation: format!("Disallowed host function: {}", field_name),
1023 });
1024 }
1025
1026 Ok(())
1027 }
1028
1029 fn validate_exports(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1031 let _module_info = module.resources_required();
1032
1033 let mut has_memory_export = false;
1035 let mut function_exports = 0;
1036
1037 for export in module.exports() {
1038 match export.ty() {
1039 wasmtime::ExternType::Memory(_) => {
1040 has_memory_export = true;
1041 }
1042 wasmtime::ExternType::Func(_) => {
1043 function_exports += 1;
1044
1045 }
1048 _ => {
1049 }
1051 }
1052 }
1053
1054 if !has_memory_export {
1056 return Err(PluginLoaderError::ValidationError {
1057 message: "WASM module must export memory".to_string(),
1058 });
1059 }
1060
1061 if function_exports > 1000 {
1063 return Err(PluginLoaderError::SecurityViolation {
1064 violation: format!("Too many function exports: {} (max: 1000)", function_exports),
1065 });
1066 }
1067
1068 Ok(())
1069 }
1070
1071 fn validate_memory_usage(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1073 let _module_info = module.resources_required();
1074
1075 for import in module.imports() {
1076 if let wasmtime::ExternType::Memory(memory_type) = import.ty() {
1077 if let Some(max) = memory_type.maximum() {
1079 if max > 100 {
1080 return Err(PluginLoaderError::SecurityViolation {
1082 violation: format!("Memory limit too high: {} pages (max: 100)", max),
1083 });
1084 }
1085 }
1086
1087 if memory_type.maximum().is_none() && memory_type.is_shared() {
1089 return Err(PluginLoaderError::SecurityViolation {
1090 violation: "Shared memory without maximum limit not allowed".to_string(),
1091 });
1092 }
1093 }
1094 }
1095
1096 for export in module.exports() {
1098 if let wasmtime::ExternType::Memory(memory_type) = export.ty() {
1099 if let Some(max) = memory_type.maximum() {
1100 if max > 100 {
1101 return Err(PluginLoaderError::SecurityViolation {
1102 violation: format!("Exported memory limit too high: {} pages", max),
1103 });
1104 }
1105 }
1106 }
1107 }
1108
1109 Ok(())
1110 }
1111
1112 fn check_dangerous_operations(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1114 let _module_info = module.resources_required();
1121
1122 self.validate_function_sizes(module)?;
1124
1125 let suspicious_imports = ["env", "wasi_unstable", "wasi_experimental"];
1127 for import in module.imports() {
1128 if suspicious_imports.contains(&import.module()) {
1129 return Err(PluginLoaderError::SecurityViolation {
1130 violation: format!("Suspicious import module: {}", import.module()),
1131 });
1132 }
1133 }
1134
1135 Ok(())
1136 }
1137
1138 fn validate_function_limits(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1140 let _module_info = module.resources_required();
1141
1142 let mut function_count = 0;
1143 for export in module.exports() {
1144 if let wasmtime::ExternType::Func(_) = export.ty() {
1145 function_count += 1;
1146 }
1147 }
1148
1149 for import in module.imports() {
1151 if let wasmtime::ExternType::Func(_) = import.ty() {
1152 function_count += 1;
1153 }
1154 }
1155
1156 if function_count > 10000 {
1158 return Err(PluginLoaderError::SecurityViolation {
1159 violation: format!("Too many functions: {} (max: 10000)", function_count),
1160 });
1161 }
1162
1163 Ok(())
1164 }
1165
1166 fn validate_function_sizes(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1168 for export in module.exports() {
1171 if let wasmtime::ExternType::Func(func_type) = export.ty() {
1172 let param_count = func_type.params().len();
1175 let result_count = func_type.results().len();
1176
1177 if param_count > 20 || result_count > 10 {
1179 return Err(PluginLoaderError::SecurityViolation {
1180 violation: format!(
1181 "Function '{}' has suspiciously complex signature: {} params, {} results",
1182 export.name(), param_count, result_count
1183 ),
1184 });
1185 }
1186
1187 let mut complex_types = 0;
1189 for param in func_type.params() {
1190 match param {
1191 wasmtime::ValType::V128 | wasmtime::ValType::Ref(_) => {
1192 complex_types += 1;
1193 }
1194 _ => {}
1195 }
1196 }
1197
1198 if complex_types > param_count / 2 && param_count > 5 {
1199 return Err(PluginLoaderError::SecurityViolation {
1200 violation: format!(
1201 "Function '{}' has unusually complex parameter types: {} complex types out of {} params",
1202 export.name(), complex_types, param_count
1203 ),
1204 });
1205 }
1206 }
1207 }
1208
1209 let mut total_functions = 0;
1211 for export in module.exports() {
1212 if let wasmtime::ExternType::Func(_) = export.ty() {
1213 total_functions += 1;
1214 }
1215 }
1216 for import in module.imports() {
1217 if let wasmtime::ExternType::Func(_) = import.ty() {
1218 total_functions += 1;
1219 }
1220 }
1221
1222 if total_functions > 5000 {
1225 return Err(PluginLoaderError::SecurityViolation {
1226 violation: format!(
1227 "Too many functions: {} (reasonable limit: 5000)",
1228 total_functions
1229 ),
1230 });
1231 }
1232
1233 Ok(())
1238 }
1239
1240 fn validate_data_segments(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1242 let wasm_bytes = module.serialize().map_err(|e| PluginLoaderError::ValidationError {
1247 message: format!("Failed to serialize WASM module: {}", e),
1248 })?;
1249
1250 let parser = Parser::new(0);
1252 let payloads =
1253 parser.parse_all(&wasm_bytes).collect::<Result<Vec<_>, _>>().map_err(|e| {
1254 PluginLoaderError::ValidationError {
1255 message: format!("Failed to parse WASM module: {}", e),
1256 }
1257 })?;
1258
1259 let suspicious_patterns = [
1261 "http://",
1262 "https://",
1263 "/bin/",
1264 "/usr/bin/",
1265 "eval(",
1266 "exec(",
1267 "system(",
1268 "shell",
1269 "cmd.exe",
1270 "powershell",
1271 "wget",
1272 "curl",
1273 "nc ",
1274 "netcat",
1275 "python -c",
1276 "ruby -e",
1277 "node -e",
1278 "bash -c",
1279 "sh -c",
1280 ];
1281
1282 for payload in payloads {
1284 if let Payload::DataSection(data_section) = payload {
1285 for data_segment_result in data_section {
1286 let data_segment =
1287 data_segment_result.map_err(|e| PluginLoaderError::ValidationError {
1288 message: format!("Failed to read data segment: {}", e),
1289 })?;
1290 let data = data_segment.data;
1291
1292 if let Ok(data_str) = std::str::from_utf8(data) {
1294 for pattern in &suspicious_patterns {
1295 if data_str.contains(pattern) {
1296 return Err(PluginLoaderError::SecurityViolation {
1297 violation: format!(
1298 "Data segment contains suspicious content: '{}'",
1299 pattern
1300 ),
1301 });
1302 }
1303 }
1304 } else {
1305 for pattern in &suspicious_patterns {
1307 if data
1308 .windows(pattern.len())
1309 .any(|window| window == pattern.as_bytes())
1310 {
1311 return Err(PluginLoaderError::SecurityViolation {
1312 violation: format!(
1313 "Data segment contains suspicious content: '{}'",
1314 pattern
1315 ),
1316 });
1317 }
1318 }
1319 }
1320 }
1321 }
1322 }
1323
1324 Ok(())
1325 }
1326
1327 pub fn allow_network_access(&self) -> bool {
1329 self.allow_network_access
1330 }
1331
1332 pub fn allow_filesystem_read(&self) -> bool {
1334 self.allow_filesystem_read
1335 }
1336
1337 pub fn allow_filesystem_write(&self) -> bool {
1339 self.allow_filesystem_write
1340 }
1341
1342 pub fn max_memory_bytes(&self) -> usize {
1344 10 * 1024 * 1024 }
1346
1347 pub fn max_cpu_percent(&self) -> f64 {
1349 0.5 }
1351}
1352
1353#[derive(Debug, Clone)]
1355pub struct ValidationSummary {
1356 pub is_valid: bool,
1358 pub manifest_valid: bool,
1360 pub wasm_valid: bool,
1362 pub manifest: Option<PluginManifest>,
1364 pub errors: Vec<String>,
1366 pub warnings: Vec<String>,
1368}
1369
1370impl Default for ValidationSummary {
1371 fn default() -> Self {
1372 Self {
1373 is_valid: true,
1374 manifest_valid: false,
1375 wasm_valid: false,
1376 manifest: None,
1377 errors: Vec::new(),
1378 warnings: Vec::new(),
1379 }
1380 }
1381}
1382
1383impl ValidationSummary {
1384 pub fn add_error<S: Into<String>>(&mut self, error: S) {
1386 self.errors.push(error.into());
1387 self.is_valid = false;
1388 }
1389
1390 pub fn add_warning<S: Into<String>>(&mut self, warning: S) {
1392 self.warnings.push(warning.into());
1393 }
1394
1395 pub fn has_errors(&self) -> bool {
1397 !self.errors.is_empty()
1398 }
1399
1400 pub fn has_warnings(&self) -> bool {
1402 !self.warnings.is_empty()
1403 }
1404
1405 pub fn error_count(&self) -> usize {
1407 self.errors.len()
1408 }
1409
1410 pub fn warning_count(&self) -> usize {
1412 self.warnings.len()
1413 }
1414}
1415
1416#[cfg(test)]
1417mod tests {
1418 use super::*;
1419
1420 #[tokio::test]
1421 async fn test_security_policies_creation() {
1422 let policies = SecurityPolicies::default();
1423 assert!(!policies.allow_network_access());
1424 assert!(!policies.allow_filesystem_read());
1425 assert!(!policies.allow_filesystem_write());
1426 assert_eq!(policies.max_memory_bytes(), 10 * 1024 * 1024);
1427 assert_eq!(policies.max_cpu_percent(), 0.5);
1428 }
1429
1430 #[tokio::test]
1431 async fn test_validation_summary() {
1432 let mut summary = ValidationSummary::default();
1433 assert!(summary.is_valid);
1434 assert!(!summary.has_errors());
1435 assert!(!summary.has_warnings());
1436
1437 summary.add_error("Test error");
1438 assert!(!summary.is_valid);
1439 assert!(summary.has_errors());
1440 assert_eq!(summary.error_count(), 1);
1441
1442 summary.add_warning("Test warning");
1443 assert!(summary.has_warnings());
1444 assert_eq!(summary.warning_count(), 1);
1445 }
1446
1447 #[tokio::test]
1448 async fn test_plugin_validator_creation() {
1449 let config = PluginLoaderConfig::default();
1450 let _validator = PluginValidator::new(config);
1451 }
1454}