Skip to main content
Glama

tfmcp

by nwiizo
security.rs15.8 kB
use anyhow::Result; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env; use std::fs; use std::path::{Path, PathBuf}; /// Security policy configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SecurityPolicy { /// Whether dangerous operations (apply, destroy) are allowed pub allow_dangerous_operations: bool, /// Whether auto-approve is allowed for apply/destroy operations pub allow_auto_approve: bool, /// List of allowed Terraform commands pub allowed_commands: Vec<String>, /// List of blocked file patterns (e.g., production configs) pub blocked_file_patterns: Vec<String>, /// Maximum number of resources that can be managed pub max_resource_limit: Option<usize>, /// Required approval patterns for certain operations pub approval_patterns: HashMap<String, String>, /// Audit logging configuration pub audit_logging: AuditConfig, } /// Audit logging configuration #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AuditConfig { /// Whether audit logging is enabled pub enabled: bool, /// Path to audit log file pub log_file: Option<PathBuf>, /// Whether to log sensitive information (state files, etc.) pub log_sensitive: bool, } /// Audit log entry #[derive(Debug, Serialize, Deserialize)] pub struct AuditLogEntry { pub timestamp: DateTime<Utc>, pub user: String, pub operation: String, pub directory: String, pub command: Vec<String>, pub success: bool, pub error: Option<String>, pub resource_count: Option<usize>, } /// Security manager for tfmcp operations pub struct SecurityManager { pub policy: SecurityPolicy, pub audit_log: Option<PathBuf>, } impl Default for SecurityPolicy { fn default() -> Self { Self { allow_dangerous_operations: false, allow_auto_approve: false, allowed_commands: vec![ "version".to_string(), "init".to_string(), "validate".to_string(), "plan".to_string(), "show".to_string(), "state".to_string(), ], blocked_file_patterns: vec![ "**/prod*/**".to_string(), "**/production*/**".to_string(), "**/*prod*.tf".to_string(), "**/*production*.tf".to_string(), "**/*secret*".to_string(), ], max_resource_limit: Some(50), approval_patterns: HashMap::new(), audit_logging: AuditConfig { enabled: true, log_file: None, log_sensitive: false, }, } } } impl SecurityManager { pub fn new() -> Result<Self> { let policy = Self::load_security_policy()?; let audit_log = if policy.audit_logging.enabled { policy .audit_logging .log_file .clone() .or_else(|| dirs::home_dir().map(|d| d.join(".tfmcp").join("audit.log"))) } else { None }; Ok(Self { policy, audit_log }) } /// Load security policy from environment variables and config files fn load_security_policy() -> Result<SecurityPolicy> { let mut policy = SecurityPolicy::default(); // Check environment variables for security settings if let Ok(val) = env::var("TFMCP_ALLOW_DANGEROUS_OPS") { policy.allow_dangerous_operations = val.to_lowercase() == "true"; } if let Ok(val) = env::var("TFMCP_ALLOW_AUTO_APPROVE") { policy.allow_auto_approve = val.to_lowercase() == "true"; } if let Ok(val) = env::var("TFMCP_MAX_RESOURCES") { if let Ok(limit) = val.parse::<usize>() { policy.max_resource_limit = Some(limit); } } if let Ok(val) = env::var("TFMCP_AUDIT_ENABLED") { policy.audit_logging.enabled = val.to_lowercase() == "true"; } if let Ok(val) = env::var("TFMCP_AUDIT_LOG_SENSITIVE") { policy.audit_logging.log_sensitive = val.to_lowercase() == "true"; } if let Ok(path) = env::var("TFMCP_AUDIT_LOG_FILE") { policy.audit_logging.log_file = Some(PathBuf::from(path)); } // Load additional security policy from config file if exists if let Some(home) = dirs::home_dir() { let config_path = home.join(".tfmcp").join("security.json"); if config_path.exists() { if let Ok(content) = fs::read_to_string(&config_path) { if let Ok(file_policy) = serde_json::from_str::<SecurityPolicy>(&content) { // Merge with environment-based policy policy = file_policy; // Re-apply environment overrides if let Ok(val) = env::var("TFMCP_ALLOW_DANGEROUS_OPS") { policy.allow_dangerous_operations = val.to_lowercase() == "true"; } } } } } Ok(policy) } /// Check if a Terraform command is allowed pub fn is_command_allowed(&self, command: &str) -> bool { // Special handling for dangerous operations match command { "apply" | "destroy" => self.policy.allow_dangerous_operations, _ => self.policy.allowed_commands.contains(&command.to_string()), } } /// Check if auto-approve is allowed for the given command pub fn is_auto_approve_allowed(&self, command: &str) -> bool { match command { "apply" | "destroy" => { self.policy.allow_dangerous_operations && self.policy.allow_auto_approve } _ => true, // Auto-approve is always allowed for safe commands } } /// Check if a file path is blocked by security policy pub fn is_file_blocked(&self, file_path: &Path) -> bool { let path_str = file_path.to_string_lossy().to_lowercase(); for pattern in &self.policy.blocked_file_patterns { let pattern_lower = pattern.to_lowercase(); // Handle different glob patterns if pattern_lower.contains("**") { // Pattern like "**/prod*/**" or "**/*prod*.tf" let pattern_parts: Vec<&str> = pattern_lower.split("**").collect(); if pattern_parts.len() == 3 { // Pattern: **/xxx/** let middle = pattern_parts[1]; // For patterns like "**/prod*/**", the middle part is "/prod*/" // We need to handle this properly if middle.starts_with('/') && middle.ends_with('/') { let inner = &middle[1..middle.len() - 1]; // Remove leading and trailing '/' if inner.contains('*') { // Handle wildcards in the middle part let inner_parts: Vec<&str> = inner.split('*').collect(); if inner_parts.len() == 2 { let prefix = inner_parts[0]; let suffix = inner_parts[1]; // Look for /prefix*suffix/ pattern in path for segment in path_str.split('/') { if segment.starts_with(prefix) && segment.ends_with(suffix) { return true; } } } } else { // Exact match for middle directory if path_str.contains(&format!("/{}/", inner)) { return true; } } } else if path_str.contains(middle) { return true; } } else if pattern_parts.len() == 2 { // Pattern: **/xxx or xxx/** let prefix = pattern_parts[0]; let suffix = pattern_parts[1]; if prefix.is_empty() && path_str.ends_with(suffix) { // Pattern: **/xxx return true; } else if suffix.is_empty() && path_str.contains(prefix) { // Pattern: xxx/** return true; } else if path_str.contains(prefix) && path_str.ends_with(suffix) { // Pattern: prefix**suffix return true; } } } else if pattern_lower.contains('*') { // Simple wildcard matching let parts: Vec<&str> = pattern_lower.split('*').collect(); let mut pos = 0; let mut matched = true; for (i, part) in parts.iter().enumerate() { if i == 0 && !part.is_empty() { // First part must match from the beginning if !path_str.starts_with(part) { matched = false; break; } pos = part.len(); } else if i == parts.len() - 1 && !part.is_empty() { // Last part must match at the end if !path_str.ends_with(part) { matched = false; break; } } else if !part.is_empty() { // Middle parts must be found in order if let Some(found_pos) = path_str[pos..].find(part) { pos += found_pos + part.len(); } else { matched = false; break; } } } if matched { return true; } } else if path_str.contains(&pattern_lower) { // Exact substring matching return true; } } false } /// Check if the number of resources exceeds the limit pub fn check_resource_limit(&self, resource_count: usize) -> Result<()> { if let Some(limit) = self.policy.max_resource_limit { if resource_count > limit { return Err(anyhow::anyhow!( "Operation blocked: Resource count ({}) exceeds security limit ({})", resource_count, limit )); } } Ok(()) } /// Log an audit entry pub fn log_audit_entry(&self, entry: AuditLogEntry) -> Result<()> { if !self.policy.audit_logging.enabled { return Ok(()); } if let Some(log_file) = &self.audit_log { // Ensure the directory exists if let Some(parent) = log_file.parent() { fs::create_dir_all(parent)?; } let log_line = serde_json::to_string(&entry)?; use std::io::Write; let mut file = fs::OpenOptions::new() .create(true) .append(true) .open(log_file)?; file.write_all(format!("{}\n", log_line).as_bytes())?; } Ok(()) } /// Create an audit log entry for a Terraform operation pub fn create_audit_entry( &self, operation: &str, directory: &str, command: &[String], success: bool, error: Option<String>, resource_count: Option<usize>, ) -> AuditLogEntry { AuditLogEntry { timestamp: Utc::now(), user: env::var("USER") .or_else(|_| env::var("USERNAME")) .unwrap_or_else(|_| "unknown".to_string()), operation: operation.to_string(), directory: directory.to_string(), command: command.to_vec(), success, error, resource_count, } } /// Get current security policy (for reporting/debugging) #[allow(dead_code)] pub fn get_policy(&self) -> &SecurityPolicy { &self.policy } /// Validate a directory for security compliance pub fn validate_directory(&self, directory: &Path) -> Result<()> { if self.is_file_blocked(directory) { return Err(anyhow::anyhow!( "Directory access blocked by security policy: {}", directory.display() )); } // Check for sensitive files in the directory if directory.exists() && directory.is_dir() { for entry in fs::read_dir(directory)? { let entry = entry?; let path = entry.path(); if self.is_file_blocked(&path) { return Err(anyhow::anyhow!( "Directory contains blocked files: {}", path.display() )); } } } Ok(()) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_default_security_policy() { let policy = SecurityPolicy::default(); assert!(!policy.allow_dangerous_operations); assert!(!policy.allow_auto_approve); assert!(policy.allowed_commands.contains(&"init".to_string())); assert!(!policy.allowed_commands.contains(&"apply".to_string())); } #[test] fn test_command_security() { let manager = SecurityManager { policy: SecurityPolicy::default(), audit_log: None, }; assert!(manager.is_command_allowed("init")); assert!(manager.is_command_allowed("plan")); assert!(!manager.is_command_allowed("apply")); assert!(!manager.is_command_allowed("destroy")); } #[test] fn test_file_blocking() { let manager = SecurityManager { policy: SecurityPolicy::default(), audit_log: None, }; let prod_file = PathBuf::from("/some/path/prod/main.tf"); let production_file = PathBuf::from("/some/path/production.tf"); let safe_file = PathBuf::from("/some/path/dev/main.tf"); assert!(manager.is_file_blocked(&prod_file)); assert!(manager.is_file_blocked(&production_file)); assert!(!manager.is_file_blocked(&safe_file)); } #[test] fn test_resource_limit() { let manager = SecurityManager { policy: SecurityPolicy { max_resource_limit: Some(10), ..SecurityPolicy::default() }, audit_log: None, }; assert!(manager.check_resource_limit(5).is_ok()); assert!(manager.check_resource_limit(15).is_err()); } #[test] fn test_audit_entry_creation() { let manager = SecurityManager { policy: SecurityPolicy::default(), audit_log: None, }; let entry = manager.create_audit_entry( "plan", "/test/dir", &["terraform".to_string(), "plan".to_string()], true, None, Some(5), ); assert_eq!(entry.operation, "plan"); assert_eq!(entry.directory, "/test/dir"); assert!(entry.success); assert_eq!(entry.resource_count, Some(5)); } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/nwiizo/tfmcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server