Skip to main content

mockforge_plugin_loader/
validator.rs

1//! Plugin validation system
2//!
3//! This module provides comprehensive plugin validation including:
4//! - Manifest validation
5//! - Capability checking
6//! - WebAssembly module validation
7//! - Security policy enforcement
8
9use super::*;
10use std::collections::HashSet;
11use std::path::Path;
12
13// Import types from plugin core
14use mockforge_plugin_core::{
15    FilesystemPermissions, NetworkPermissions, PluginCapabilities, PluginId, PluginManifest,
16    PluginVersion, ResourceLimits,
17};
18
19// WASM parsing
20use wasmparser::{Parser, Payload};
21
22// Cryptography
23use ring::signature;
24
25// Path expansion
26use shellexpand;
27
28// Encoding
29use base64::{engine::general_purpose, Engine as _};
30use hex;
31
32// JSON serialization
33use serde_json;
34
35/// Plugin signature information
36#[derive(Debug, Clone)]
37struct PluginSignature {
38    algorithm: String,
39    signature: Vec<u8>,
40    key_id: String,
41}
42
43/// Plugin validator
44pub struct PluginValidator {
45    /// Loader configuration
46    config: PluginLoaderConfig,
47    /// Security policies
48    security_policies: SecurityPolicies,
49}
50
51impl PluginValidator {
52    /// Create a new plugin validator
53    pub fn new(config: PluginLoaderConfig) -> Self {
54        Self {
55            config,
56            security_policies: SecurityPolicies::default(),
57        }
58    }
59
60    /// Validate plugin manifest
61    pub async fn validate_manifest(&self, manifest: &PluginManifest) -> LoaderResult<()> {
62        let mut errors = Vec::new();
63
64        // Validate basic manifest structure
65        if let Err(validation_error) = manifest.validate() {
66            errors.push(PluginLoaderError::manifest(validation_error));
67        }
68
69        // Validate security policies
70        if let Err(e) = self.security_policies.validate_manifest(manifest) {
71            errors.push(e);
72        }
73
74        // Validate plugin dependencies
75        let mut visited = 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    /// Validate plugin capabilities
96    pub fn validate_capabilities(&self, capability_names: &[String]) -> LoaderResult<()> {
97        // Convert string capability names to PluginCapabilities struct
98        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    /// Validate WebAssembly file
111    pub async fn validate_wasm_file(&self, wasm_path: &Path) -> LoaderResult<()> {
112        // Check file exists and is readable
113        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        // Check file size limits
126        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        // Validate WASM module structure
135        self.validate_wasm_module(wasm_path).await?;
136
137        Ok(())
138    }
139
140    /// Validate plugin file (complete plugin directory)
141    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        // Find manifest file
151        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        // Load and validate manifest
157        let manifest = PluginManifest::from_file(&manifest_path)
158            .map_err(|e| PluginLoaderError::manifest(format!("Failed to load manifest: {}", e)))?;
159
160        // Validate manifest
161        self.validate_manifest(&manifest).await?;
162
163        // Check for WASM file
164        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        // Validate WASM file (unless skipped for testing)
184        if !self.config.skip_wasm_validation {
185            self.validate_wasm_file(&wasm_files[0]).await?;
186        }
187
188        Ok(manifest)
189    }
190
191    /// Validate plugin dependencies
192    async fn validate_dependencies(
193        &self,
194        current_plugin_id: &PluginId,
195        dependencies: &std::collections::HashMap<mockforge_plugin_core::PluginId, PluginVersion>,
196        visited: &mut HashSet<PluginId>,
197    ) -> LoaderResult<()> {
198        for (plugin_id, version) in dependencies {
199            // Check for circular dependencies using DFS
200            if self.would_create_circular_dependency(current_plugin_id, plugin_id, visited) {
201                return Err(PluginLoaderError::ValidationError {
202                    message: format!(
203                        "Circular dependency detected: '{}' -> '{}'",
204                        current_plugin_id.0, plugin_id.0
205                    ),
206                });
207            }
208
209            // Validate dependency ID format
210            if plugin_id.0.is_empty() {
211                return Err(PluginLoaderError::ValidationError {
212                    message: "Dependency plugin ID cannot be empty".to_string(),
213                });
214            }
215
216            if plugin_id.0.len() > 100 {
217                return Err(PluginLoaderError::ValidationError {
218                    message: format!(
219                        "Dependency plugin ID '{}' is too long (max 100 characters)",
220                        plugin_id.0
221                    ),
222                });
223            }
224
225            // Validate version requirements
226            if version.major == 0 && version.minor == 0 && version.patch == 0 {
227                tracing::warn!("Dependency '{}' specifies version 0.0.0 which may indicate development/testing", plugin_id.0);
228            }
229
230            // Check for potentially problematic dependency patterns
231            if plugin_id.0.contains("..") || plugin_id.0.contains("/") || plugin_id.0.contains("\\")
232            {
233                return Err(PluginLoaderError::SecurityViolation {
234                    violation: format!(
235                        "Dependency plugin ID '{}' contains potentially unsafe characters",
236                        plugin_id.0
237                    ),
238                });
239            }
240
241            // Future enhancements would check:
242            // - If dependency is installed and available
243            // - Version compatibility with installed versions
244            // - API compatibility
245            // - Security status and known vulnerabilities
246        }
247
248        Ok(())
249    }
250
251    /// Validate a dependency version requirement
252    ///
253    /// Check if adding this dependency would create a circular dependency
254    fn would_create_circular_dependency(
255        &self,
256        current_plugin_id: &PluginId,
257        dependency_id: &PluginId,
258        visited: &mut HashSet<PluginId>,
259    ) -> bool {
260        // Check for direct self-dependency
261        if dependency_id == current_plugin_id {
262            return true;
263        }
264
265        // Check if this dependency creates a cycle in the current validation path
266        // Note: Full cycle detection would require loading dependency manifests
267        // and recursively validating their dependencies. This is a simplified check
268        // that prevents obvious cycles during manifest validation.
269
270        visited.contains(dependency_id)
271    }
272
273    /// Validate WebAssembly module structure
274    async fn validate_wasm_module(&self, wasm_path: &Path) -> LoaderResult<()> {
275        let bytes = tokio::fs::read(wasm_path)
276            .await
277            .map_err(|e| PluginLoaderError::fs(format!("Cannot read WASM module: {e}")))?;
278        self.security_policies.validate_wasm_binary(&bytes)
279    }
280
281    /// Check if plugin is signed (if signing is required)
282    pub async fn validate_plugin_signature(
283        &self,
284        plugin_path: &Path,
285        manifest: &PluginManifest,
286    ) -> LoaderResult<()> {
287        // Check if signature validation is required
288        if self.config.allow_unsigned {
289            return Ok(());
290        }
291
292        // Look for signature file alongside the plugin
293        let sig_path = plugin_path.with_extension("sig");
294        if !sig_path.exists() {
295            return Err(PluginLoaderError::SecurityViolation {
296                violation: format!(
297                    "Plugin '{}' requires a signature but none was found",
298                    manifest.info.id.0
299                ),
300            });
301        }
302
303        // Read signature file
304        let signature_data =
305            std::fs::read(&sig_path).map_err(|e| PluginLoaderError::ValidationError {
306                message: format!("Failed to read signature file: {}", e),
307            })?;
308
309        // Parse signature (assuming it's a simple format for now)
310        let signature = self.parse_signature(&signature_data)?;
311
312        // Read plugin data for verification
313        let plugin_data =
314            std::fs::read(plugin_path).map_err(|e| PluginLoaderError::ValidationError {
315                message: format!("Failed to read plugin file: {}", e),
316            })?;
317
318        // Verify signature against trusted keys
319        self.verify_signature(&plugin_data, &signature, manifest).await?;
320
321        Ok(())
322    }
323
324    /// Parse signature data
325    fn parse_signature(&self, data: &[u8]) -> Result<PluginSignature, PluginLoaderError> {
326        // Parse signature in JSON format for better structure and extensibility
327        // Format: {"algorithm": "rsa|ecdsa|ed25519", "signature": "hex_string", "key_id": "key_identifier"}
328
329        let sig_json: serde_json::Value =
330            serde_json::from_slice(data).map_err(|e| PluginLoaderError::ValidationError {
331                message: format!("Invalid signature JSON format: {}", e),
332            })?;
333
334        let algorithm = sig_json
335            .get("algorithm")
336            .and_then(|v| v.as_str())
337            .ok_or_else(|| PluginLoaderError::ValidationError {
338                message: "Missing or invalid 'algorithm' field".to_string(),
339            })?
340            .to_string();
341
342        let signature_hex =
343            sig_json.get("signature").and_then(|v| v.as_str()).ok_or_else(|| {
344                PluginLoaderError::ValidationError {
345                    message: "Missing or invalid 'signature' field".to_string(),
346                }
347            })?;
348
349        let key_id = sig_json
350            .get("key_id")
351            .and_then(|v| v.as_str())
352            .ok_or_else(|| PluginLoaderError::ValidationError {
353                message: "Missing or invalid 'key_id' field".to_string(),
354            })?
355            .to_string();
356
357        // Validate algorithm
358        if !["rsa", "ecdsa", "ed25519"].contains(&algorithm.as_str()) {
359            return Err(PluginLoaderError::ValidationError {
360                message: format!("Unsupported signature algorithm: {}", algorithm),
361            });
362        }
363
364        // Decode signature
365        let signature =
366            hex::decode(signature_hex).map_err(|e| PluginLoaderError::ValidationError {
367                message: format!("Invalid signature hex: {}", e),
368            })?;
369
370        Ok(PluginSignature {
371            algorithm,
372            signature,
373            key_id,
374        })
375    }
376
377    /// Verify signature against trusted keys
378    async fn verify_signature(
379        &self,
380        data: &[u8],
381        signature: &PluginSignature,
382        manifest: &PluginManifest,
383    ) -> LoaderResult<()> {
384        // Get trusted public key for this key_id
385        let public_key = self.get_trusted_key(&signature.key_id)?;
386
387        // Verify signature based on algorithm
388        match signature.algorithm.as_str() {
389            "rsa" => {
390                // Verify RSA signature using the ring cryptography library
391                self.verify_rsa_signature(data, &signature.signature, &public_key)?;
392            }
393            "ecdsa" => {
394                // ECDSA signature verification
395                self.verify_ecdsa_signature(data, &signature.signature, &public_key)?;
396            }
397            "ed25519" => {
398                // Ed25519 signature verification
399                self.verify_ed25519_signature(data, &signature.signature, &public_key)?;
400            }
401            _ => {
402                return Err(PluginLoaderError::ValidationError {
403                    message: format!("Unsupported algorithm: {}", signature.algorithm),
404                });
405            }
406        }
407
408        // Additional validation: check if key is authorized for this plugin
409        self.validate_key_authorization(&signature.key_id, manifest)?;
410
411        Ok(())
412    }
413
414    /// Get trusted public key for verification
415    fn get_trusted_key(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
416        // First check if the key is in our trusted keys list
417        if !self.config.trusted_keys.contains(&key_id.to_string()) {
418            return Err(PluginLoaderError::SecurityViolation {
419                violation: format!("Key '{}' is not in the trusted keys list", key_id),
420            });
421        }
422
423        // Load key from configured sources (environment, config, filesystem, etc.)
424        // In production, keys should be loaded from secure storage like a key store, database, or HSM
425        self.load_key_from_store(key_id)
426    }
427
428    /// Load a key from the key store (environment, config, file system, etc.)
429    fn load_key_from_store(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
430        // 1. Check environment variables first
431        if let Ok(key_data) = self.load_key_from_env(key_id) {
432            tracing::info!("Loaded key '{}' from environment variable", key_id);
433            return Ok(key_data);
434        }
435
436        // 2. Check configuration key data
437        if let Some(key_data) = self.config.key_data.get(key_id) {
438            tracing::info!("Loaded key '{}' from configuration", key_id);
439            return Ok(key_data.clone());
440        }
441
442        // 3. Check file system (fallback for backward compatibility)
443        if let Ok(key_data) = self.load_key_from_filesystem(key_id) {
444            tracing::info!("Loaded key '{}' from filesystem", key_id);
445            return Ok(key_data);
446        }
447
448        // Future extensions: uncomment and implement as needed
449        // 4. Query a database for the key
450        if let Ok(key_data) = self.load_key_from_database(key_id) {
451            tracing::info!("Loaded key '{}' from database provider", key_id);
452            return Ok(key_data);
453        }
454
455        // 5. Call a key management service
456        if let Ok(key_data) = self.load_key_from_kms(key_id) {
457            tracing::info!("Loaded key '{}' from key management service", key_id);
458            return Ok(key_data);
459        }
460
461        Err(PluginLoaderError::SecurityViolation {
462            violation: format!("Could not find key data for trusted key: {}", key_id),
463        })
464    }
465
466    /// Load key from environment variables
467    fn load_key_from_env(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
468        self.load_key_material_from_prefixes(key_id, &["MOCKFORGE_KEY"], "environment")
469    }
470
471    fn load_key_material_from_prefixes(
472        &self,
473        key_id: &str,
474        prefixes: &[&str],
475        source_name: &str,
476    ) -> Result<Vec<u8>, PluginLoaderError> {
477        let normalized = key_id.to_uppercase().replace("-", "_");
478
479        for prefix in prefixes {
480            // Try base64-encoded key first
481            let b64_env_key = format!("{}_{}_B64", prefix, normalized);
482            if let Ok(b64_value) = std::env::var(&b64_env_key) {
483                match general_purpose::STANDARD.decode(&b64_value) {
484                    Ok(key_data) => return Ok(key_data),
485                    Err(e) => {
486                        tracing::warn!(
487                            "Failed to decode base64 key from {} ({}): {}",
488                            b64_env_key,
489                            source_name,
490                            e
491                        );
492                    }
493                }
494            }
495
496            // Try hex-encoded key
497            let hex_env_key = format!("{}_{}_HEX", prefix, normalized);
498            if let Ok(hex_value) = std::env::var(&hex_env_key) {
499                match hex::decode(&hex_value) {
500                    Ok(key_data) => return Ok(key_data),
501                    Err(e) => {
502                        tracing::warn!(
503                            "Failed to decode hex key from {} ({}): {}",
504                            hex_env_key,
505                            source_name,
506                            e
507                        );
508                    }
509                }
510            }
511
512            // Try raw key data
513            let raw_env_key = format!("{}_{}", prefix, normalized);
514            if let Ok(key_data) = std::env::var(&raw_env_key) {
515                return Ok(key_data.into_bytes());
516            }
517        }
518
519        Err(PluginLoaderError::SecurityViolation {
520            violation: format!("Key not found in {}: {}", source_name, key_id),
521        })
522    }
523
524    fn load_key_from_directory(
525        &self,
526        key_id: &str,
527        dir: &std::path::Path,
528    ) -> Result<Vec<u8>, PluginLoaderError> {
529        let candidates = [
530            dir.join(format!("{}.der", key_id)),
531            dir.join(format!("{}.pem", key_id)),
532            dir.join(format!("{}.key", key_id)),
533            dir.join(format!("{}.bin", key_id)),
534        ];
535
536        for path in candidates {
537            if path.exists() {
538                return std::fs::read(&path).map_err(|e| PluginLoaderError::SecurityViolation {
539                    violation: format!("Failed to read key file {}: {}", path.display(), e),
540                });
541            }
542        }
543
544        Err(PluginLoaderError::SecurityViolation {
545            violation: format!("Key '{}' not found in directory {}", key_id, dir.display()),
546        })
547    }
548
549    /// Load key from filesystem (legacy support)
550    fn load_key_from_filesystem(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
551        let key_paths = vec![
552            format!("~/.mockforge/keys/{}.der", key_id),
553            format!("~/.mockforge/keys/{}.pem", key_id),
554            format!("/etc/mockforge/keys/{}.der", key_id),
555            format!("/etc/mockforge/keys/{}.pem", key_id),
556        ];
557
558        for key_path in key_paths {
559            let expanded_path = shellexpand::tilde(&key_path);
560            let path = std::path::Path::new(expanded_path.as_ref());
561
562            if path.exists() {
563                match std::fs::read(path) {
564                    Ok(key_data) => return Ok(key_data),
565                    Err(e) => {
566                        tracing::warn!("Failed to read key file {}: {}", path.display(), e);
567                        continue;
568                    }
569                }
570            }
571        }
572
573        Err(PluginLoaderError::SecurityViolation {
574            violation: format!("Key not found in filesystem: {}", key_id),
575        })
576    }
577
578    /// Load key from database
579    /// Checks for database configuration via environment variables:
580    /// - MOCKFORGE_DB_TYPE: sqlite, postgres, mysql
581    /// - MOCKFORGE_DB_CONNECTION: connection string
582    /// - MOCKFORGE_DB_KEY_TABLE: table name for keys (default: plugin_keys)
583    #[allow(unused)]
584    fn load_key_from_database(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
585        let db_type = std::env::var("MOCKFORGE_DB_TYPE").map_err(|_| {
586            PluginLoaderError::SecurityViolation {
587                violation: "Database key loading requires MOCKFORGE_DB_TYPE environment variable"
588                    .to_string(),
589            }
590        })?;
591
592        let connection_string = std::env::var("MOCKFORGE_DB_CONNECTION").map_err(|_| {
593            PluginLoaderError::SecurityViolation {
594                violation:
595                    "Database key loading requires MOCKFORGE_DB_CONNECTION environment variable"
596                        .to_string(),
597            }
598        })?;
599
600        let table_name =
601            std::env::var("MOCKFORGE_DB_KEY_TABLE").unwrap_or_else(|_| "plugin_keys".to_string());
602
603        tracing::info!("Database key loading configured: type={}, table={}", db_type, table_name);
604        tracing::debug!("Looking up key '{}' in database-backed key source", key_id);
605
606        if let Ok(key_data) =
607            self.load_key_material_from_prefixes(key_id, &["MOCKFORGE_DB_KEY"], "database env")
608        {
609            return Ok(key_data);
610        }
611
612        if let Ok(key_dir) = std::env::var("MOCKFORGE_DB_KEY_DIR") {
613            let expanded = shellexpand::tilde(&key_dir);
614            let path = std::path::Path::new(expanded.as_ref());
615            if path.exists() {
616                return self.load_key_from_directory(key_id, path);
617            }
618        }
619
620        Err(PluginLoaderError::SecurityViolation {
621            violation: format!(
622                "Database key '{}' not found in configured environment or key directory (connection: {})",
623                key_id, connection_string
624            ),
625        })
626    }
627
628    /// Load key from key management service
629    /// Supports multiple KMS providers via environment variables:
630    /// - MOCKFORGE_KMS_PROVIDER: aws, vault, azure, gcp
631    /// - MOCKFORGE_KMS_REGION: AWS region (for AWS KMS)
632    /// - MOCKFORGE_VAULT_ADDR: HashCorp Vault address
633    /// - MOCKFORGE_VAULT_TOKEN: HashCorp Vault token
634    #[allow(unused)]
635    fn load_key_from_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
636        let kms_provider = std::env::var("MOCKFORGE_KMS_PROVIDER").map_err(|_| {
637            PluginLoaderError::SecurityViolation {
638                violation: "KMS key loading requires MOCKFORGE_KMS_PROVIDER environment variable"
639                    .to_string(),
640            }
641        })?;
642
643        match kms_provider.to_lowercase().as_str() {
644            "aws" => self.load_key_from_aws_kms(key_id),
645            "vault" => self.load_key_from_vault(key_id),
646            "azure" => self.load_key_from_azure_kv(key_id),
647            "gcp" => self.load_key_from_gcp_kms(key_id),
648            _ => Err(PluginLoaderError::SecurityViolation {
649                violation: format!("Unsupported KMS provider: {}", kms_provider),
650            }),
651        }
652    }
653
654    /// Load key from AWS KMS
655    fn load_key_from_aws_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
656        let region =
657            std::env::var("MOCKFORGE_KMS_REGION").unwrap_or_else(|_| "us-east-1".to_string());
658
659        tracing::info!("AWS KMS key loading configured: region={}", region);
660        tracing::debug!("Looking up key '{}' in AWS KMS", key_id);
661
662        self.load_key_material_from_prefixes(
663            key_id,
664            &["MOCKFORGE_AWS_KMS_KEY", "MOCKFORGE_KMS_KEY"],
665            "AWS KMS",
666        )
667    }
668
669    /// Load key from HashCorp Vault
670    fn load_key_from_vault(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
671        let vault_addr = std::env::var("MOCKFORGE_VAULT_ADDR").map_err(|_| {
672            PluginLoaderError::SecurityViolation {
673                violation: "Vault key loading requires MOCKFORGE_VAULT_ADDR environment variable"
674                    .to_string(),
675            }
676        })?;
677
678        let _vault_token = std::env::var("MOCKFORGE_VAULT_TOKEN").map_err(|_| {
679            PluginLoaderError::SecurityViolation {
680                violation: "Vault key loading requires MOCKFORGE_VAULT_TOKEN environment variable"
681                    .to_string(),
682            }
683        })?;
684
685        tracing::info!("HashCorp Vault key loading configured: addr={}", vault_addr);
686        tracing::debug!("Looking up key '{}' in Vault", key_id);
687
688        self.load_key_material_from_prefixes(
689            key_id,
690            &["MOCKFORGE_VAULT_KEY", "MOCKFORGE_KMS_KEY"],
691            "Vault",
692        )
693    }
694
695    /// Load key from Azure Key Vault
696    fn load_key_from_azure_kv(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
697        tracing::info!("Azure Key Vault key loading requested");
698        tracing::debug!("Looking up key '{}' in Azure Key Vault", key_id);
699
700        self.load_key_material_from_prefixes(
701            key_id,
702            &["MOCKFORGE_AZURE_KV_KEY", "MOCKFORGE_KMS_KEY"],
703            "Azure Key Vault",
704        )
705    }
706
707    /// Load key from Google Cloud KMS
708    fn load_key_from_gcp_kms(&self, key_id: &str) -> Result<Vec<u8>, PluginLoaderError> {
709        tracing::info!("Google Cloud KMS key loading requested");
710        tracing::debug!("Looking up key '{}' in GCP KMS", key_id);
711
712        self.load_key_material_from_prefixes(
713            key_id,
714            &["MOCKFORGE_GCP_KMS_KEY", "MOCKFORGE_KMS_KEY"],
715            "Google Cloud KMS",
716        )
717    }
718
719    /// Verify RSA signature
720    fn verify_rsa_signature(
721        &self,
722        data: &[u8],
723        signature: &[u8],
724        public_key: &[u8],
725    ) -> LoaderResult<()> {
726        // Create an unparsed public key from the DER-encoded key
727        let public_key =
728            signature::UnparsedPublicKey::new(&signature::RSA_PKCS1_2048_8192_SHA256, public_key);
729
730        // Verify the signature
731        public_key
732            .verify(data, signature)
733            .map_err(|e| PluginLoaderError::SecurityViolation {
734                violation: format!("RSA signature verification failed: {}", e),
735            })?;
736
737        Ok(())
738    }
739
740    /// Verify ECDSA signature
741    fn verify_ecdsa_signature(
742        &self,
743        data: &[u8],
744        signature: &[u8],
745        public_key: &[u8],
746    ) -> LoaderResult<()> {
747        // Create an unparsed public key from the DER-encoded key
748        let public_key =
749            signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_ASN1, public_key);
750
751        // Verify the signature
752        public_key
753            .verify(data, signature)
754            .map_err(|e| PluginLoaderError::SecurityViolation {
755                violation: format!("ECDSA signature verification failed: {}", e),
756            })?;
757
758        Ok(())
759    }
760
761    /// Verify Ed25519 signature
762    fn verify_ed25519_signature(
763        &self,
764        data: &[u8],
765        signature: &[u8],
766        public_key: &[u8],
767    ) -> LoaderResult<()> {
768        // Create an unparsed public key from the raw key bytes
769        let public_key = signature::UnparsedPublicKey::new(&signature::ED25519, public_key);
770
771        // Verify the signature
772        public_key
773            .verify(data, signature)
774            .map_err(|e| PluginLoaderError::SecurityViolation {
775                violation: format!("Ed25519 signature verification failed: {}", e),
776            })?;
777
778        Ok(())
779    }
780
781    /// Validate that the key is authorized for this plugin
782    fn validate_key_authorization(
783        &self,
784        key_id: &str,
785        manifest: &PluginManifest,
786    ) -> LoaderResult<()> {
787        // Check if this key is authorized to sign plugins from this author
788        if self.config.trusted_keys.contains(&key_id.to_string()) {
789            return Ok(());
790        }
791
792        Err(PluginLoaderError::SecurityViolation {
793            violation: format!(
794                "Key '{}' is not authorized to sign plugins from '{}'",
795                key_id, manifest.info.author.name
796            ),
797        })
798    }
799
800    /// Get validation summary for a plugin
801    pub async fn get_validation_summary(&self, plugin_path: &Path) -> ValidationSummary {
802        let mut summary = ValidationSummary::default();
803
804        // Check if path exists
805        if !plugin_path.exists() {
806            summary.errors.push("Plugin path does not exist".to_string());
807            return summary;
808        }
809
810        // Validate manifest
811        let manifest_result = self.validate_plugin_file(plugin_path).await;
812        match manifest_result {
813            Ok(manifest) => {
814                summary.manifest_valid = true;
815                summary.manifest = Some(manifest);
816            }
817            Err(e) => {
818                summary.errors.push(format!("Manifest validation failed: {}", e));
819            }
820        }
821
822        // Check WASM file
823        if let Ok(wasm_path) = self.find_wasm_file(plugin_path) {
824            let wasm_result = self.validate_wasm_file(&wasm_path).await;
825            summary.wasm_valid = wasm_result.is_ok();
826            if let Err(e) = wasm_result {
827                summary.errors.push(format!("WASM validation failed: {}", e));
828            }
829        } else {
830            summary.errors.push("No WebAssembly file found".to_string());
831        }
832
833        summary.is_valid =
834            summary.manifest_valid && summary.wasm_valid && summary.errors.is_empty();
835        summary
836    }
837
838    /// Find WASM file in plugin directory
839    fn find_wasm_file(&self, plugin_path: &Path) -> LoaderResult<PathBuf> {
840        let entries = std::fs::read_dir(plugin_path)
841            .map_err(|e| PluginLoaderError::fs(format!("Cannot read directory: {}", e)))?;
842
843        for entry in entries {
844            let entry =
845                entry.map_err(|e| PluginLoaderError::fs(format!("Cannot read entry: {}", e)))?;
846            let path = entry.path();
847
848            if let Some(extension) = path.extension() {
849                if extension == "wasm" {
850                    return Ok(path);
851                }
852            }
853        }
854
855        Err(PluginLoaderError::load("No WebAssembly file found".to_string()))
856    }
857}
858
859/// Security policies for plugin validation
860#[derive(Debug, Clone)]
861pub struct SecurityPolicies {
862    /// Maximum WASM file size in bytes
863    pub max_wasm_file_size: u64,
864    /// Allowed import modules
865    pub allowed_imports: HashSet<String>,
866    /// Forbidden import functions
867    pub forbidden_imports: HashSet<String>,
868    /// Maximum memory pages (64KB each)
869    pub max_memory_pages: u32,
870    /// Maximum number of functions
871    pub max_functions: u32,
872    /// Allow floating point operations
873    pub allow_floats: bool,
874    /// Allow SIMD operations
875    pub allow_simd: bool,
876    /// Allow network access
877    pub allow_network_access: bool,
878    /// Allow filesystem read access
879    pub allow_filesystem_read: bool,
880    /// Allow filesystem write access
881    pub allow_filesystem_write: bool,
882}
883
884impl Default for SecurityPolicies {
885    fn default() -> Self {
886        let mut allowed_imports = HashSet::new();
887        allowed_imports.insert("env".to_string());
888        allowed_imports.insert("wasi_snapshot_preview1".to_string());
889
890        let mut forbidden_imports = HashSet::new();
891        forbidden_imports.insert("abort".to_string());
892        forbidden_imports.insert("exit".to_string());
893
894        Self {
895            max_wasm_file_size: 10 * 1024 * 1024, // 10MB
896            allowed_imports,
897            forbidden_imports,
898            max_memory_pages: 256, // 16MB
899            max_functions: 1000,
900            allow_floats: true,
901            allow_simd: false,
902            allow_network_access: false,
903            allow_filesystem_read: false,
904            allow_filesystem_write: false,
905        }
906    }
907}
908
909impl SecurityPolicies {
910    /// Validate plugin manifest against security policies
911    pub fn validate_manifest(&self, manifest: &PluginManifest) -> LoaderResult<()> {
912        // Manifest validation should check for manifest structure issues,
913        // but capability restrictions should be enforced at runtime, not validation time.
914        // This allows manifests to declare capabilities that may be restricted based on deployment.
915
916        // Check for dangerous capabilities that are always forbidden
917        let _caps = PluginCapabilities::from_strings(&manifest.capabilities);
918
919        // For now, we allow all capability declarations in manifests
920        // Runtime enforcement will restrict actual usage
921
922        Ok(())
923    }
924
925    /// Validate plugin capabilities
926    pub fn validate_capabilities(&self, capabilities: &PluginCapabilities) -> LoaderResult<()> {
927        // Check resource limits
928        if capabilities.resources.max_memory_bytes > self.max_memory_bytes() {
929            return Err(PluginLoaderError::security(format!(
930                "Memory limit {} exceeds maximum allowed {}",
931                capabilities.resources.max_memory_bytes,
932                self.max_memory_bytes()
933            )));
934        }
935
936        if capabilities.resources.max_cpu_percent > self.max_cpu_percent() {
937            return Err(PluginLoaderError::security(format!(
938                "CPU limit {:.2}% exceeds maximum allowed {:.2}%",
939                capabilities.resources.max_cpu_percent,
940                self.max_cpu_percent()
941            )));
942        }
943
944        Ok(())
945    }
946
947    /// Validate the original WebAssembly binary, including its data segments.
948    pub fn validate_wasm_binary(&self, bytes: &[u8]) -> LoaderResult<()> {
949        let module = wasmtime::Module::new(&wasmtime::Engine::default(), bytes)
950            .map_err(|e| PluginLoaderError::wasm(format!("Invalid WASM module: {e}")))?;
951        self.validate_wasm_module(&module)?;
952        self.validate_data_segments(bytes)
953    }
954
955    /// Validate a compiled module's structure and limits. Use
956    /// [`Self::validate_wasm_binary`] to check the original data segments too;
957    /// Wasmtime serialization contains native compiled code, not WASM bytes.
958    pub fn validate_wasm_module(&self, module: &wasmtime::Module) -> LoaderResult<()> {
959        // Perform sophisticated WASM module validation
960
961        // 1. Check import signatures - ensure only allowed imports
962        self.validate_imports(module)?;
963
964        // 2. Check export signatures - ensure required exports are present
965        self.validate_exports(module)?;
966
967        // 3. Validate memory usage and limits
968        self.validate_memory_usage(module)?;
969
970        // 4. Check for dangerous operations
971        self.check_dangerous_operations(module)?;
972
973        // 5. Verify function count limits
974        self.validate_function_limits(module)?;
975
976        Ok(())
977    }
978
979    /// Validate WASM imports against allowed signatures
980    fn validate_imports(&self, module: &wasmtime::Module) -> LoaderResult<()> {
981        // Get module information
982        let _module_info = module.resources_required();
983
984        // Check each import
985        for import in module.imports() {
986            let module_name = import.module();
987            let field_name = import.name();
988
989            // Allow only specific WASI imports and our custom host functions
990            let allowed_modules = [
991                "wasi_snapshot_preview1",
992                "wasi:io/streams",
993                "wasi:filesystem/types",
994                "mockforge:plugin/host",
995            ];
996
997            if !allowed_modules.contains(&module_name) {
998                return Err(PluginLoaderError::SecurityViolation {
999                    violation: format!("Disallowed import module: {}", module_name),
1000                });
1001            }
1002
1003            // Validate specific imports within allowed modules
1004            match module_name {
1005                "wasi_snapshot_preview1" => {
1006                    self.validate_wasi_import(field_name)?;
1007                }
1008                "mockforge:plugin/host" => {
1009                    self.validate_host_import(field_name)?;
1010                }
1011                _ => {
1012                    // For other allowed modules, we could add specific validation
1013                }
1014            }
1015        }
1016
1017        Ok(())
1018    }
1019
1020    /// Validate WASI imports
1021    fn validate_wasi_import(&self, field_name: &str) -> LoaderResult<()> {
1022        // Allow common safe WASI functions
1023        let allowed_functions = [
1024            // File operations (with capability checks)
1025            "fd_read",
1026            "fd_write",
1027            "fd_close",
1028            "fd_fdstat_get",
1029            // Path operations (with capability checks)
1030            "path_open",
1031            "path_readlink",
1032            "path_filestat_get",
1033            // Time operations
1034            "clock_time_get",
1035            // Process operations
1036            "proc_exit",
1037            // Random operations
1038            "random_get",
1039        ];
1040
1041        if !allowed_functions.contains(&field_name) {
1042            return Err(PluginLoaderError::SecurityViolation {
1043                violation: format!("Disallowed WASI function: {}", field_name),
1044            });
1045        }
1046
1047        Ok(())
1048    }
1049
1050    /// Validate host function imports
1051    fn validate_host_import(&self, field_name: &str) -> LoaderResult<()> {
1052        // Allow specific host functions that plugins can call
1053        let allowed_functions = [
1054            "log_message",
1055            "get_config_value",
1056            "store_data",
1057            "retrieve_data",
1058        ];
1059
1060        if !allowed_functions.contains(&field_name) {
1061            return Err(PluginLoaderError::SecurityViolation {
1062                violation: format!("Disallowed host function: {}", field_name),
1063            });
1064        }
1065
1066        Ok(())
1067    }
1068
1069    /// Validate WASM exports
1070    fn validate_exports(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1071        let _module_info = module.resources_required();
1072
1073        // Check for required exports
1074        let mut has_memory_export = false;
1075        let mut function_exports = 0;
1076
1077        for export in module.exports() {
1078            match export.ty() {
1079                wasmtime::ExternType::Memory(_) => {
1080                    has_memory_export = true;
1081                }
1082                wasmtime::ExternType::Func(_) => {
1083                    function_exports += 1;
1084
1085                    // Validate function signature if needed
1086                    // For now, we just count them
1087                }
1088                _ => {
1089                    // Other export types (tables, globals) are allowed
1090                }
1091            }
1092        }
1093
1094        // Every WASM module should have at least one memory export
1095        if !has_memory_export {
1096            return Err(PluginLoaderError::ValidationError {
1097                message: "WASM module must export memory".to_string(),
1098            });
1099        }
1100
1101        // Check function export limits
1102        if function_exports > 1000 {
1103            return Err(PluginLoaderError::SecurityViolation {
1104                violation: format!("Too many function exports: {} (max: 1000)", function_exports),
1105            });
1106        }
1107
1108        Ok(())
1109    }
1110
1111    /// Validate memory usage and limits
1112    fn validate_memory_usage(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1113        let _module_info = module.resources_required();
1114
1115        for import in module.imports() {
1116            if let wasmtime::ExternType::Memory(memory_type) = import.ty() {
1117                // Check memory limits
1118                if let Some(max) = memory_type.maximum() {
1119                    if max > 100 {
1120                        // 100 pages = 6.4MB
1121                        return Err(PluginLoaderError::SecurityViolation {
1122                            violation: format!("Memory limit too high: {} pages (max: 100)", max),
1123                        });
1124                    }
1125                }
1126
1127                // Check if memory can grow beyond safe limits
1128                if memory_type.maximum().is_none() && memory_type.is_shared() {
1129                    return Err(PluginLoaderError::SecurityViolation {
1130                        violation: "Shared memory without maximum limit not allowed".to_string(),
1131                    });
1132                }
1133            }
1134        }
1135
1136        // Check exported memories
1137        for export in module.exports() {
1138            if let wasmtime::ExternType::Memory(memory_type) = export.ty() {
1139                if let Some(max) = memory_type.maximum() {
1140                    if max > 100 {
1141                        return Err(PluginLoaderError::SecurityViolation {
1142                            violation: format!("Exported memory limit too high: {} pages", max),
1143                        });
1144                    }
1145                }
1146            }
1147        }
1148
1149        Ok(())
1150    }
1151
1152    /// Check for dangerous operations in the WASM module
1153    fn check_dangerous_operations(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1154        // This would require more sophisticated analysis of the WASM bytecode
1155        // For now, we'll do basic checks
1156
1157        // Check for potentially dangerous instruction patterns
1158        // This is a placeholder for more advanced static analysis
1159
1160        let _module_info = module.resources_required();
1161
1162        // Check function sizes (large functions might be obfuscated malicious code)
1163        self.validate_function_sizes(module)?;
1164
1165        // Check for suspicious import patterns
1166        let suspicious_imports = ["env", "wasi_unstable", "wasi_experimental"];
1167        for import in module.imports() {
1168            if suspicious_imports.contains(&import.module()) {
1169                return Err(PluginLoaderError::SecurityViolation {
1170                    violation: format!("Suspicious import module: {}", import.module()),
1171                });
1172            }
1173        }
1174
1175        Ok(())
1176    }
1177
1178    /// Validate function count limits
1179    fn validate_function_limits(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1180        let _module_info = module.resources_required();
1181
1182        let mut function_count = 0;
1183        for export in module.exports() {
1184            if let wasmtime::ExternType::Func(_) = export.ty() {
1185                function_count += 1;
1186            }
1187        }
1188
1189        // Also count imported functions
1190        for import in module.imports() {
1191            if let wasmtime::ExternType::Func(_) = import.ty() {
1192                function_count += 1;
1193            }
1194        }
1195
1196        // Set reasonable limits
1197        if function_count > 10000 {
1198            return Err(PluginLoaderError::SecurityViolation {
1199                violation: format!("Too many functions: {} (max: 10000)", function_count),
1200            });
1201        }
1202
1203        Ok(())
1204    }
1205
1206    /// Validate function sizes to detect potentially malicious code
1207    fn validate_function_sizes(&self, module: &wasmtime::Module) -> LoaderResult<()> {
1208        // Check exported functions for suspicious characteristics that may indicate
1209        // malicious or obfuscated code
1210        for export in module.exports() {
1211            if let wasmtime::ExternType::Func(func_type) = export.ty() {
1212                // Check if the function has too many parameters/results
1213                // Large functions often have complex signatures
1214                let param_count = func_type.params().len();
1215                let result_count = func_type.results().len();
1216
1217                // Flag functions with suspiciously complex signatures
1218                if param_count > 20 || result_count > 10 {
1219                    return Err(PluginLoaderError::SecurityViolation {
1220                        violation: format!(
1221                            "Function '{}' has suspiciously complex signature: {} params, {} results",
1222                            export.name(), param_count, result_count
1223                        ),
1224                    });
1225                }
1226
1227                // Check for unusual parameter types that might indicate obfuscation
1228                let mut complex_types = 0;
1229                for param in func_type.params() {
1230                    match param {
1231                        wasmtime::ValType::V128 | wasmtime::ValType::Ref(_) => {
1232                            complex_types += 1;
1233                        }
1234                        _ => {}
1235                    }
1236                }
1237
1238                if complex_types > param_count / 2 && param_count > 5 {
1239                    return Err(PluginLoaderError::SecurityViolation {
1240                        violation: format!(
1241                            "Function '{}' has unusually complex parameter types: {} complex types out of {} params",
1242                            export.name(), complex_types, param_count
1243                        ),
1244                    });
1245                }
1246            }
1247        }
1248
1249        // Count total functions as an indicator of potential obfuscation
1250        let mut total_functions = 0;
1251        for export in module.exports() {
1252            if let wasmtime::ExternType::Func(_) = export.ty() {
1253                total_functions += 1;
1254            }
1255        }
1256        for import in module.imports() {
1257            if let wasmtime::ExternType::Func(_) = import.ty() {
1258                total_functions += 1;
1259            }
1260        }
1261
1262        // Flag modules with an excessive number of functions
1263        // (could indicate obfuscated malicious code)
1264        if total_functions > 5000 {
1265            return Err(PluginLoaderError::SecurityViolation {
1266                violation: format!(
1267                    "Too many functions: {} (reasonable limit: 5000)",
1268                    total_functions
1269                ),
1270            });
1271        }
1272
1273        // For actual function size checking, we would need to parse the WASM binary
1274        // and examine the code section. This implementation provides structural
1275        // validation that can detect some forms of malicious code.
1276
1277        Ok(())
1278    }
1279
1280    /// Validate data segments for malicious content
1281    fn validate_data_segments(&self, wasm_bytes: &[u8]) -> LoaderResult<()> {
1282        // Check data segments for potentially malicious content
1283        // Scan for suspicious strings, URLs, shell commands, etc.
1284
1285        // Parse the WASM binary to extract data segments
1286        let parser = Parser::new(0);
1287        let payloads =
1288            parser.parse_all(wasm_bytes).collect::<Result<Vec<_>, _>>().map_err(|e| {
1289                PluginLoaderError::ValidationError {
1290                    message: format!("Failed to parse WASM module: {}", e),
1291                }
1292            })?;
1293
1294        // Define suspicious patterns to check for
1295        let suspicious_patterns = [
1296            "http://",
1297            "https://",
1298            "/bin/",
1299            "/usr/bin/",
1300            "eval(",
1301            "exec(",
1302            "system(",
1303            "shell",
1304            "cmd.exe",
1305            "powershell",
1306            "wget",
1307            "curl",
1308            "nc ",
1309            "netcat",
1310            "python -c",
1311            "ruby -e",
1312            "node -e",
1313            "bash -c",
1314            "sh -c",
1315        ];
1316
1317        // Check each payload for data sections
1318        for payload in payloads {
1319            if let Payload::DataSection(data_section) = payload {
1320                for data_segment_result in data_section {
1321                    let data_segment =
1322                        data_segment_result.map_err(|e| PluginLoaderError::ValidationError {
1323                            message: format!("Failed to read data segment: {}", e),
1324                        })?;
1325                    let data = data_segment.data;
1326
1327                    // Convert to string for easier checking (assuming UTF-8)
1328                    if let Ok(data_str) = std::str::from_utf8(data) {
1329                        for pattern in &suspicious_patterns {
1330                            if data_str.contains(pattern) {
1331                                return Err(PluginLoaderError::SecurityViolation {
1332                                    violation: format!(
1333                                        "Data segment contains suspicious content: '{}'",
1334                                        pattern
1335                                    ),
1336                                });
1337                            }
1338                        }
1339                    } else {
1340                        // If not UTF-8, check for byte sequences
1341                        for pattern in &suspicious_patterns {
1342                            if data
1343                                .windows(pattern.len())
1344                                .any(|window| window == pattern.as_bytes())
1345                            {
1346                                return Err(PluginLoaderError::SecurityViolation {
1347                                    violation: format!(
1348                                        "Data segment contains suspicious content: '{}'",
1349                                        pattern
1350                                    ),
1351                                });
1352                            }
1353                        }
1354                    }
1355                }
1356            }
1357        }
1358
1359        Ok(())
1360    }
1361
1362    /// Check if network access is allowed
1363    pub fn allow_network_access(&self) -> bool {
1364        self.allow_network_access
1365    }
1366
1367    /// Check if file system read access is allowed
1368    pub fn allow_filesystem_read(&self) -> bool {
1369        self.allow_filesystem_read
1370    }
1371
1372    /// Check if file system write access is allowed
1373    pub fn allow_filesystem_write(&self) -> bool {
1374        self.allow_filesystem_write
1375    }
1376
1377    /// Get maximum allowed memory in bytes
1378    pub fn max_memory_bytes(&self) -> usize {
1379        10 * 1024 * 1024 // 10MB
1380    }
1381
1382    /// Get maximum allowed CPU usage
1383    pub fn max_cpu_percent(&self) -> f64 {
1384        0.5 // 50%
1385    }
1386}
1387
1388/// Validation summary for a plugin
1389#[derive(Debug, Clone)]
1390pub struct ValidationSummary {
1391    /// Whether the plugin is valid overall
1392    pub is_valid: bool,
1393    /// Whether the manifest is valid
1394    pub manifest_valid: bool,
1395    /// Whether the WASM file is valid
1396    pub wasm_valid: bool,
1397    /// Plugin manifest (if loaded successfully)
1398    pub manifest: Option<PluginManifest>,
1399    /// Validation errors
1400    pub errors: Vec<String>,
1401    /// Validation warnings
1402    pub warnings: Vec<String>,
1403}
1404
1405impl Default for ValidationSummary {
1406    fn default() -> Self {
1407        Self {
1408            is_valid: true,
1409            manifest_valid: false,
1410            wasm_valid: false,
1411            manifest: None,
1412            errors: Vec::new(),
1413            warnings: Vec::new(),
1414        }
1415    }
1416}
1417
1418impl ValidationSummary {
1419    /// Add an error
1420    pub fn add_error<S: Into<String>>(&mut self, error: S) {
1421        self.errors.push(error.into());
1422        self.is_valid = false;
1423    }
1424
1425    /// Add a warning
1426    pub fn add_warning<S: Into<String>>(&mut self, warning: S) {
1427        self.warnings.push(warning.into());
1428    }
1429
1430    /// Check if there are any errors
1431    pub fn has_errors(&self) -> bool {
1432        !self.errors.is_empty()
1433    }
1434
1435    /// Check if there are any warnings
1436    pub fn has_warnings(&self) -> bool {
1437        !self.warnings.is_empty()
1438    }
1439
1440    /// Get error count
1441    pub fn error_count(&self) -> usize {
1442        self.errors.len()
1443    }
1444
1445    /// Get warning count
1446    pub fn warning_count(&self) -> usize {
1447        self.warnings.len()
1448    }
1449}
1450
1451#[cfg(test)]
1452mod tests {
1453    use super::*;
1454
1455    #[tokio::test]
1456    async fn test_security_policies_creation() {
1457        let policies = SecurityPolicies::default();
1458        assert!(!policies.allow_network_access());
1459        assert!(!policies.allow_filesystem_read());
1460        assert!(!policies.allow_filesystem_write());
1461        assert_eq!(policies.max_memory_bytes(), 10 * 1024 * 1024);
1462        assert_eq!(policies.max_cpu_percent(), 0.5);
1463    }
1464
1465    #[tokio::test]
1466    async fn test_validation_summary() {
1467        let mut summary = ValidationSummary::default();
1468        assert!(summary.is_valid);
1469        assert!(!summary.has_errors());
1470        assert!(!summary.has_warnings());
1471
1472        summary.add_error("Test error");
1473        assert!(!summary.is_valid);
1474        assert!(summary.has_errors());
1475        assert_eq!(summary.error_count(), 1);
1476
1477        summary.add_warning("Test warning");
1478        assert!(summary.has_warnings());
1479        assert_eq!(summary.warning_count(), 1);
1480    }
1481
1482    #[tokio::test]
1483    async fn test_plugin_validator_creation() {
1484        let config = PluginLoaderConfig::default();
1485        let _validator = PluginValidator::new(config);
1486        // Basic smoke test - validator was created successfully
1487        // Test passes if no panic occurs during creation
1488    }
1489}
1490
1491#[cfg(test)]
1492mod original_binary_regressions {
1493    use super::SecurityPolicies;
1494    const WASM: &[u8] = b"\0asm\x01\0\0\0\x05\x04\x01\x01\x01\x01\x07\x0a\x01\x06memory\x02\0";
1495
1496    #[test]
1497    fn loadable_original_binary_passes_structural_and_data_checks() {
1498        SecurityPolicies::default().validate_wasm_binary(WASM).unwrap();
1499    }
1500
1501    #[test]
1502    fn suspicious_data_is_rejected_in_utf8_and_binary_segments() {
1503        for data in [b"/bin/sh".as_slice(), b"\xff/bin/sh".as_slice()] {
1504            let mut wasm = WASM.to_vec();
1505            wasm.extend_from_slice(&[
1506                11,
1507                (6 + data.len()) as u8,
1508                1,
1509                0,
1510                0x41,
1511                0,
1512                0x0b,
1513                data.len() as u8,
1514            ]);
1515            wasm.extend_from_slice(data);
1516            let error = SecurityPolicies::default().validate_wasm_binary(&wasm).unwrap_err();
1517            assert!(error.is_security_error(), "{error}");
1518            assert!(error.to_string().contains("suspicious content"), "{error}");
1519        }
1520    }
1521
1522    #[test]
1523    fn original_binary_validation_still_requires_memory_export() {
1524        let error = SecurityPolicies::default()
1525            .validate_wasm_binary(b"\0asm\x01\0\0\0")
1526            .unwrap_err();
1527        assert!(error.to_string().contains("must export memory"), "{error}");
1528    }
1529}