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