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