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}