From 2fa59057fafc5d382c9468cc58c537568f2d8bbe Mon Sep 17 00:00:00 2001 From: "Rodrigo Rodriguez (Pragmatismo)" Date: Tue, 31 Mar 2026 19:55:16 -0300 Subject: [PATCH] fix: Resolve migration error, Vault 403, cache timeout, and shell injection false positives - Fix migration 6.2.5: Create lost_reason column before VIEW that references it - Fix Vault 403: Enable KV2 secrets engine after initialization - Fix cache timeout: Increase Valkey readiness wait from 12s to 30s - Fix command_guard: Remove () from forbidden chars (safe in std::process::Command) --- migrations/6.2.5-crm-department-sla/up.sql | 10 ++++---- src/core/bootstrap/bootstrap_manager.rs | 8 +++---- src/core/package_manager/installer.rs | 27 ++++++++++++++++++++++ src/security/command_guard.rs | 26 ++++++++++++++++++++- 4 files changed, 61 insertions(+), 10 deletions(-) diff --git a/migrations/6.2.5-crm-department-sla/up.sql b/migrations/6.2.5-crm-department-sla/up.sql index 656ae888..adbe47b3 100644 --- a/migrations/6.2.5-crm-department-sla/up.sql +++ b/migrations/6.2.5-crm-department-sla/up.sql @@ -52,7 +52,10 @@ INSERT INTO attendance_sla_policies (org_id, bot_id, name, channel, priority, fi SELECT DISTINCT org_id, id as bot_id, 'Default - Low', NULL, 'low', 60, 1440 FROM bots WHERE org_id IS NOT NULL ON CONFLICT DO NOTHING; --- 5. Create legacy compat views for leads/opportunities (from crm-sales.md) +-- 5. Add lost_reason column BEFORE views that reference it +ALTER TABLE crm_deals ADD COLUMN IF NOT EXISTS lost_reason varchar(255); + +-- 6. Create legacy compat views for leads/opportunities (from crm-sales.md) CREATE OR REPLACE VIEW crm_leads_compat AS SELECT id, org_id, bot_id, contact_id, account_id, COALESCE(title, name, '') as title, description, value, currency, @@ -71,11 +74,8 @@ CREATE OR REPLACE VIEW crm_opportunities_compat AS FROM crm_deals WHERE stage IN ('proposal', 'negotiation', 'won', 'lost'); --- 6. Create index for SLA events +-- 7. Create index for SLA events CREATE INDEX IF NOT EXISTS idx_sla_events_status ON attendance_sla_events(status); CREATE INDEX IF NOT EXISTS idx_sla_events_due ON attendance_sla_events(due_at); CREATE INDEX IF NOT EXISTS idx_sla_events_session ON attendance_sla_events(session_id); CREATE INDEX IF NOT EXISTS idx_sla_policies_org_bot ON attendance_sla_policies(org_id, bot_id); - --- 7. Add lost_reason column if not exists -ALTER TABLE crm_deals ADD COLUMN IF NOT EXISTS lost_reason varchar(255); diff --git a/src/core/bootstrap/bootstrap_manager.rs b/src/core/bootstrap/bootstrap_manager.rs index ac2ca0a9..c4fb0127 100644 --- a/src/core/bootstrap/bootstrap_manager.rs +++ b/src/core/bootstrap/bootstrap_manager.rs @@ -127,15 +127,15 @@ impl BootstrapManager { match pm.start("cache") { Ok(_child) => { info!("Valkey cache process started, waiting for readiness..."); - // Wait for cache to be ready - for i in 0..12 { + // Wait for cache to be ready (up to 30 seconds) + for i in 0..30 { sleep(Duration::from_secs(1)).await; if cache_health_check() { info!("Valkey cache is responding"); break; } - if i == 11 { - warn!("Valkey cache did not respond after 12 seconds"); + if i == 29 { + warn!("Valkey cache did not respond after 30 seconds"); } } } diff --git a/src/core/package_manager/installer.rs b/src/core/package_manager/installer.rs index d8088ae4..c3e866f0 100644 --- a/src/core/package_manager/installer.rs +++ b/src/core/package_manager/installer.rs @@ -1518,6 +1518,33 @@ VAULT_CACERT={} info!("✓ Created .env with VAULT_ADDR, VAULT_TOKEN"); info!("✓ Created /opt/gbo/secrets/vault-unseal-keys (chmod 600)"); + // Enable KV2 secrets engine at 'secret/' path + info!("Enabling KV2 secrets engine at 'secret/'..."); + let enable_kv2_cmd = format!( + "VAULT_ADDR={} VAULT_TOKEN={} VAULT_CACERT={} {} secrets enable -path=secret kv-v2", + vault_addr, + root_token, + ca_cert.display(), + vault_bin.display() + ); + match safe_sh_command(&enable_kv2_cmd) { + Some(output) => { + if output.status.success() { + info!("KV2 secrets engine enabled at 'secret/'"); + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + if stderr.contains("path is already in use") { + info!("KV2 secrets engine already enabled"); + } else { + warn!("Failed to enable KV2 secrets engine: {}", stderr); + } + } + } + None => { + warn!("Failed to execute KV2 enable command"); + } + } + Ok(()) } diff --git a/src/security/command_guard.rs b/src/security/command_guard.rs index b19a3566..e281781c 100644 --- a/src/security/command_guard.rs +++ b/src/security/command_guard.rs @@ -92,7 +92,7 @@ static ALLOWED_COMMANDS: LazyLock> = LazyLock::new(|| { static FORBIDDEN_SHELL_CHARS: LazyLock> = LazyLock::new(|| { HashSet::from([ - ';', '|', '&', '$', '`', '(', ')', '{', '}', '<', '>', '\n', '\r', '\0', + ';', '|', '&', '$', '`', '<', '>', '\n', '\r', '\0', ]) }); @@ -170,6 +170,30 @@ impl SafeCommand { Ok(self) } + pub fn trusted_arg(mut self, arg: &str) -> Result { + if arg.is_empty() { + return Err(CommandGuardError::InvalidArgument( + "Empty argument".to_string(), + )); + } + if arg.len() > 4096 { + return Err(CommandGuardError::InvalidArgument( + "Argument too long".to_string(), + )); + } + let dangerous_patterns = ["$(", "`", "&&", "||", ">>", "<<", ".."]; + for pattern in dangerous_patterns { + if arg.contains(pattern) { + return Err(CommandGuardError::ShellInjectionAttempt(format!( + "Dangerous pattern '{}' detected", + pattern + ))); + } + } + self.args.push(arg.to_string()); + Ok(self) + } + pub fn shell_script_arg(mut self, script: &str) -> Result { let is_unix_shell = self.command == "bash" || self.command == "sh"; let is_windows_cmd = self.command == "cmd";