fix: Resolve migration error, Vault 403, cache timeout, and shell injection false positives
Some checks failed
BotServer CI/CD / build (push) Has been cancelled

- 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)
This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-31 19:55:16 -03:00
parent 26684e2db3
commit 2fa59057fa
4 changed files with 61 additions and 10 deletions

View file

@ -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);

View file

@ -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");
}
}
}

View file

@ -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(())
}

View file

@ -92,7 +92,7 @@ static ALLOWED_COMMANDS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| {
static FORBIDDEN_SHELL_CHARS: LazyLock<HashSet<char>> = 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<Self, CommandGuardError> {
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<Self, CommandGuardError> {
let is_unix_shell = self.command == "bash" || self.command == "sh";
let is_windows_cmd = self.command == "cmd";