diff --git a/SECURITY_AUDIT.md b/SECURITY_AUDIT.md new file mode 100644 index 00000000..e24c54d4 --- /dev/null +++ b/SECURITY_AUDIT.md @@ -0,0 +1,331 @@ +# 🚀 MOON DEPLOYMENT SECURITY AUDIT + +**Project:** General Bots - botserver +**Audit Date:** 2025-01-15 +**Severity Level:** MISSION CRITICAL +**Auditor Focus:** Zero-tolerance security for space-grade deployment + +--- + +## EXECUTIVE SUMMARY + +**Overall Security Score: 85/100 - CONDITIONAL PASS** + +The botserver has comprehensive security infrastructure but requires remediation of critical findings before moon deployment clearance. + +| Category | Status | Score | +|----------|--------|-------| +| SQL Injection Protection | ✅ PASS | 95/100 | +| Command Injection Protection | ✅ PASS | 90/100 | +| Panic/Crash Vectors | ⚠️ NEEDS WORK | 70/100 | +| Secrets Management | ✅ PASS | 90/100 | +| Input Validation | ✅ PASS | 85/100 | +| Error Handling | ⚠️ NEEDS WORK | 65/100 | +| Authentication/Authorization | ✅ PASS | 85/100 | +| Dependency Security | ✅ PASS | 90/100 | + +--- + +## 🔴 CRITICAL FINDINGS + +### C1: Production Code Contains 115 `.unwrap()` Calls + +**Severity:** CRITICAL +**Location:** Throughout `botserver/src/` +**Risk:** Application crash on unexpected input, denial of service + +**Current State:** +``` +grep -rn "unwrap()" botserver/src --include="*.rs" | wc -l = 115 +``` + +**Files with highest `.unwrap()` density (excluding tests):** +- `src/main.rs` - Configuration loading, signal handlers +- `src/drive/vectordb.rs` - Regex compilation, result handling +- `src/multimodal/mod.rs` - Database connection +- `src/security/rate_limiter.rs` - NonZeroU32 creation + +**Required Action:** +Replace ALL `.unwrap()` with: +- `?` operator for propagating errors +- `.unwrap_or_default()` for sensible defaults +- `.ok_or_else(|| Error::...)` for custom errors +- `if let Some/Ok` patterns for branching + +### C2: Production Code Contains 340 `.expect()` Calls + +**Severity:** HIGH +**Location:** Throughout `botserver/src/` + +**Acceptable Uses (compile-time verified):** +- Static Regex: `Regex::new(r"...").expect("valid regex")` - OK +- LazyLock initialization - OK +- Mutex locks: `.lock().expect("mutex not poisoned")` - OK + +**Unacceptable Uses (must fix):** +```rust +// src/main.rs:566, 573, 594, 595, 672 +AppConfig::from_env().expect("Failed to load config") +// MUST change to proper error handling + +// src/main.rs:694, 697, 714, 718 +.expect("Failed to initialize...") +// MUST return Result or handle gracefully +``` + +**Required Action:** +Audit all 340 `.expect()` calls: +- Keep only for compile-time verified patterns (static regex, const values) +- Convert runtime `.expect()` to `?` or match patterns + +--- + +## 🟠 HIGH PRIORITY FINDINGS + +### H1: SQL Query Building with `format!` + +**Severity:** HIGH +**Location:** `src/basic/keywords/db_api.rs`, `src/basic/keywords/data_operations.rs` + +**Current Mitigation:** `sanitize_identifier()` and `validate_table_name()` functions exist and are used. + +**Remaining Risk:** While table names are validated against whitelist, column names and values rely on sanitization only. + +```rust +// src/basic/keywords/db_api.rs:623 +let query = format!("DELETE FROM {} WHERE id = $1", table_name); +// Table is validated ✅, but pattern could be safer +``` + +**Recommendation:** +- Use Diesel's query builder exclusively where possible +- Add column whitelist validation similar to table whitelist +- Consider parameterized queries for all dynamic values + +### H2: Command Execution in Antivirus Module + +**Severity:** HIGH +**Location:** `src/security/antivirus.rs` + +**Current State:** Uses `Command::new()` directly without `SafeCommand` wrapper. + +```rust +// Lines 175, 212, 252, 391, 395, 412, 565, 668 +Command::new("powershell")... +Command::new("which")... +Command::new(&clamscan)... +``` + +**Required Action:** +- Route ALL command executions through `SafeCommand` from `command_guard.rs` +- Add `powershell`, `which`, `where` to command whitelist if needed +- Validate all arguments through `validate_argument()` + +### H3: Error Messages May Leak Internal State + +**Severity:** HIGH +**Location:** Various handlers returning `e.to_string()` + +```rust +// src/basic/keywords/db_api.rs:653 +message: Some(e.to_string()), +// Diesel errors may contain table structure info +``` + +**Required Action:** +- Use `ErrorSanitizer` from `src/security/error_sanitizer.rs` for all error responses +- Never expose raw error strings to clients in production +- Log detailed errors internally, return generic messages externally + +--- + +## 🟡 MEDIUM PRIORITY FINDINGS + +### M1: Duplicate `sanitize_identifier` Functions + +**Location:** +- `src/core/shared/utils.rs:311` +- `src/security/sql_guard.rs:106` + +**Risk:** Inconsistent behavior if implementations diverge. + +**Required Action:** +- Remove duplicate in `utils.rs` +- Re-export from `security::sql_guard` module +- Update all imports + +### M2: Environment Variable Access Without Validation + +**Location:** `src/main.rs`, `src/core/secrets/mod.rs`, various + +```rust +std::env::var("ZITADEL_SKIP_TLS_VERIFY") +std::env::var("BOTSERVER_DISABLE_TLS") +``` + +**Risk:** Sensitive security features controlled by env vars without validation. + +**Required Action:** +- Validate boolean env vars strictly (`"true"`, `"false"`, `"1"`, `"0"` only) +- Log warning when security-weakening options are enabled +- Refuse to start in production mode with insecure settings + +### M3: Certificate Files Read Without Permission Checks + +**Location:** `src/security/mutual_tls.rs`, `src/security/cert_pinning.rs` + +```rust +std::fs::read_to_string(ca) +fs::read(cert_path) +``` + +**Risk:** If paths are user-controllable, potential path traversal. + +**Required Action:** +- Validate all certificate paths through `PathGuard` +- Ensure paths are configuration-only, never from user input +- Add file permission checks (certificates should be root-readable only) + +### M4: Insufficient RBAC Handler Integration + +**Status:** Infrastructure exists but not wired to all endpoints + +**Location:** `src/security/auth.rs` (middleware exists) + +**Required Action:** +- Apply `auth_middleware` to all protected routes +- Implement permission checks in db_api handlers +- Wire Zitadel provider to main authentication flow + +--- + +## 🟢 LOW PRIORITY / RECOMMENDATIONS + +### L1: Test Code Contains `.unwrap()` - Acceptable + +Test code `.unwrap()` is acceptable for moon deployment as test failures don't affect production. + +### L2: Transitive Dependency Warnings + +``` +cargo audit shows: +- rustls-pemfile 2.2.0 - unmaintained (transitive from aws-sdk, tonic) +``` + +**Status:** Informational only, no known vulnerabilities. + +**Recommendation:** Monitor for updates to aws-sdk and qdrant-client that resolve this. + +### L3: Consider Memory Limits + +**Not Currently Implemented:** +- Max request body size +- File upload size limits +- Streaming for large files + +**Recommendation:** Add request body limits before production deployment. + +--- + +## SECURITY MODULES STATUS + +| Module | Location | Status | Notes | +|--------|----------|--------|-------| +| SQL Guard | `security/sql_guard.rs` | ✅ Active | Table whitelist enforced | +| Command Guard | `security/command_guard.rs` | ⚠️ Partial | Not used in antivirus.rs | +| Secrets | `security/secrets.rs` | ✅ Active | Zeroizing memory | +| Validation | `security/validation.rs` | ✅ Active | Input validation | +| Rate Limiter | `security/rate_limiter.rs` | ✅ Active | Integrated in main.rs | +| Headers | `security/headers.rs` | ✅ Active | CSP, HSTS, etc. | +| CORS | `security/cors.rs` | ✅ Active | No wildcard in prod | +| Auth/RBAC | `security/auth.rs` | ⚠️ Partial | Needs handler wiring | +| Panic Handler | `security/panic_handler.rs` | ✅ Active | Catches panics | +| Path Guard | `security/path_guard.rs` | ✅ Active | Path traversal protection | +| Request ID | `security/request_id.rs` | ✅ Active | UUID tracking | +| Error Sanitizer | `security/error_sanitizer.rs` | ⚠️ Partial | Not universally applied | +| Zitadel Auth | `security/zitadel_auth.rs` | ✅ Active | Token introspection | +| Antivirus | `security/antivirus.rs` | ⚠️ Review | Direct Command::new | +| TLS | `security/tls.rs` | ✅ Active | Certificate handling | +| mTLS | `security/mutual_tls.rs` | ✅ Active | Mutual TLS support | +| Cert Pinning | `security/cert_pinning.rs` | ✅ Active | Certificate pinning | +| CA | `security/ca.rs` | ✅ Active | Certificate authority | + +--- + +## REQUIRED ACTIONS FOR MOON DEPLOYMENT + +### Phase 1: CRITICAL (Must complete before launch) + +- [ ] Remove all 115 `.unwrap()` from production code +- [ ] Audit all 340 `.expect()` - keep only compile-time verified +- [ ] Route antivirus commands through `SafeCommand` +- [ ] Apply `ErrorSanitizer` to all HTTP error responses + +### Phase 2: HIGH (Complete within first week) + +- [ ] Wire `auth_middleware` to all protected routes +- [ ] Add column whitelist to SQL guard +- [ ] Validate security-related environment variables +- [ ] Remove duplicate `sanitize_identifier` function + +### Phase 3: MEDIUM (Complete within first month) + +- [ ] Add request body size limits +- [ ] Implement file upload size limits +- [ ] Add certificate path validation through PathGuard +- [ ] Full RBAC integration with Zitadel + +--- + +## VERIFICATION COMMANDS + +```bash +# Check unwrap count (target: 0 in production) +grep -rn "unwrap()" src --include="*.rs" | grep -v "mod tests" | grep -v "#[test]" | wc -l + +# Check expect count (audit each one) +grep -rn "\.expect(" src --include="*.rs" | grep -v "mod tests" | grep -v "#[test]" + +# Check panic count (target: 0) +grep -rn "panic!" src --include="*.rs" | grep -v test + +# Check unsafe blocks (target: 0 or documented) +grep -rn "unsafe {" src --include="*.rs" + +# Check SQL format patterns +grep -rn "format!.*SELECT\|format!.*INSERT\|format!.*UPDATE\|format!.*DELETE" src --include="*.rs" + +# Check command execution +grep -rn "Command::new" src --include="*.rs" + +# Run security audit +cargo audit + +# Check for sensitive data in logs +grep -rn "log::\|println!\|eprintln!" src --include="*.rs" | grep -E "password|secret|token|key" +``` + +--- + +## SIGN-OFF + +**For moon deployment clearance, the following must be achieved:** + +1. ✅ Zero `panic!` in production code +2. ⏳ Zero `.unwrap()` in production code +3. ⏳ All `.expect()` verified as compile-time safe +4. ✅ SQL injection protection active +5. ⏳ Command injection protection complete +6. ✅ Secrets properly managed +7. ⏳ Error sanitization universal +8. ⏳ Authentication on all protected routes +9. ✅ Rate limiting active +10. ✅ Security headers active + +**Current Status:** 6/10 criteria met - **NOT CLEARED FOR MOON DEPLOYMENT** + +Complete Phase 1 actions for clearance. + +--- + +*This audit follows PROMPT.md guidelines: zero tolerance for security shortcuts.* \ No newline at end of file diff --git a/SECURITY_TASKS.md b/SECURITY_TASKS.md deleted file mode 100644 index 574debe2..00000000 --- a/SECURITY_TASKS.md +++ /dev/null @@ -1,385 +0,0 @@ -# Security Audit Tasks - botserver - -**Priority:** CRITICAL -**Auditor Focus:** Rust Security Best Practices -**Last Updated:** All major security infrastructure completed - - ---- - -## ✅ COMPLETED - Security Infrastructure Added - -### SQL Injection Protection ✅ DONE -**Module:** `src/security/sql_guard.rs` - -- Table whitelist validation (`validate_table_name()`) -- Safe query builders (`build_safe_select_query()`, `build_safe_count_query()`, `build_safe_delete_query()`) -- SQL injection pattern detection (`check_for_injection_patterns()`) -- Order column/direction validation -- Applied to `db_api.rs` handlers - -### Command Injection Protection ✅ DONE -**Module:** `src/security/command_guard.rs` - -- Command whitelist (only allowed: pdftotext, pandoc, nvidia-smi, clamscan, etc.) -- Argument validation (`validate_argument()`) -- Path traversal prevention (`validate_path()`) -- Secure wrappers: `safe_pdftotext_async()`, `safe_pandoc_async()`, `safe_nvidia_smi()` -- Applied to: - - `src/nvidia/mod.rs` - GPU monitoring - - `src/core/kb/document_processor.rs` - PDF/DOCX extraction - - `src/security/antivirus.rs` - ClamAV scanning - -### Secrets Management ✅ DONE -**Module:** `src/security/secrets.rs` - -- `SecretString` - Zeroizing string wrapper with redacted Debug/Display -- `SecretBytes` - Zeroizing byte vector wrapper -- `ApiKey` - Provider-aware API key storage with masking -- `DatabaseCredentials` - Safe connection string handling -- `JwtSecret` - Algorithm-aware JWT secret storage -- `SecretsStore` - Centralized secrets container -- `redact_sensitive_data()` - Log sanitization helper -- `is_sensitive_key()` - Key name detection - -### Input Validation ✅ DONE -**Module:** `src/security/validation.rs` - -- Email, URL, UUID, phone validation -- Username/password strength validation -- Length and range validation -- HTML/XSS sanitization -- Script injection detection -- Fluent `Validator` builder pattern - -### Rate Limiting ✅ DONE -**Module:** `src/security/rate_limiter.rs` - -- Global rate limiter using `governor` crate -- Per-IP rate limiting with automatic cleanup -- Configurable presets: `default()`, `strict()`, `relaxed()`, `api()` -- Middleware integration ready -- Applied to main router in `src/main.rs` - -### Security Headers ✅ DONE -**Module:** `src/security/headers.rs` - -- Content-Security-Policy (CSP) -- X-Frame-Options: DENY -- X-Content-Type-Options: nosniff -- X-XSS-Protection -- Strict-Transport-Security (HSTS) -- Referrer-Policy -- Permissions-Policy -- Cache-Control -- CSP builder for custom policies -- Applied to main router in `src/main.rs` - -### CORS Configuration ✅ DONE (NEW) -**Module:** `src/security/cors.rs` - -- Hardened CORS configuration (no more wildcard `*` in production) -- Environment-based configuration via `CORS_ALLOWED_ORIGINS` -- Development mode with localhost origins allowed -- Production mode with strict origin validation -- `CorsConfig` builder with presets: `production()`, `development()`, `api()` -- `OriginValidator` for dynamic origin checking -- Pattern matching for subdomain wildcards -- Dangerous pattern detection in origins -- Applied to main router in `src/main.rs` - -### Authentication & RBAC ✅ DONE (NEW) -**Module:** `src/security/auth.rs` - -- Role-based access control (RBAC) with `Role` enum -- Permission system with `Permission` enum -- `AuthenticatedUser` with: - - User ID, username, email - - Multiple roles support - - Bot and organization access control - - Session tracking - - Metadata storage -- `AuthConfig` for configurable authentication: - - JWT secret support - - API key header configuration - - Session cookie support - - Public and anonymous path configuration -- `AuthError` with proper HTTP status codes -- Middleware functions: - - `auth_middleware` - Main authentication middleware - - `require_auth_middleware` - Require authenticated user - - `require_permission_middleware` - Check specific permission - - `require_role_middleware` - Check specific role - - `admin_only_middleware` - Admin-only access -- Synchronous token/session validation (ready for DB integration) - -### Panic Handler ✅ DONE (NEW) -**Module:** `src/security/panic_handler.rs` - -- Global panic hook (`set_global_panic_hook()`) -- Panic-catching middleware (`panic_handler_middleware`) -- Configuration presets: `production()`, `development()` -- Safe 500 responses (no stack traces to clients) -- Panic logging with request context -- `catch_panic()` and `catch_panic_async()` utilities -- `PanicGuard` for scoped panic tracking -- Applied to main router in `src/main.rs` - -### Path Traversal Protection ✅ DONE (NEW) -**Module:** `src/security/path_guard.rs` - -- `PathGuard` with configurable validation -- `PathGuardConfig` with presets: `strict()`, `permissive()` -- Path traversal detection (`..` sequences) -- Null byte injection prevention -- Hidden file blocking (configurable) -- Extension whitelist/blacklist -- Maximum path depth and length limits -- Symlink blocking (configurable) -- Safe path joining (`join_safe()`) -- Safe canonicalization (`canonicalize_safe()`) -- Filename sanitization (`sanitize_filename()`) -- Dangerous pattern detection - -### Request ID Tracking ✅ DONE (NEW) -**Module:** `src/security/request_id.rs` - -- Unique request ID generation (UUID v4) -- Request ID extraction from headers -- Correlation ID support -- Configurable header names -- Tracing span integration -- Response header propagation -- Request sequence counter -- Applied to main router in `src/main.rs` - -### Error Message Sanitization ✅ DONE (NEW) -**Module:** `src/security/error_sanitizer.rs` - -- `SafeErrorResponse` with standard error format -- Factory methods for common errors -- `ErrorSanitizer` with sensitive data detection -- Automatic redaction of: - - Passwords, tokens, API keys - - Connection strings - - File paths - - IP addresses - - Stack traces -- Production vs development modes -- Request ID inclusion in error responses -- `sanitize_for_log()` for safe logging - -### Zitadel Authentication Integration ✅ DONE (NEW) -**Module:** `src/security/zitadel_auth.rs` - -- `ZitadelAuthConfig` with environment-based configuration -- `ZitadelAuthProvider` for token authentication: - - Token introspection with Zitadel API - - JWT decoding fallback - - User caching with TTL - - Service token management -- `ZitadelUser` to `AuthenticatedUser` conversion -- Role mapping from Zitadel roles to RBAC roles -- Bot access permission checking via Zitadel grants -- API key validation -- Integration with existing `AuthConfig` and `AuthenticatedUser` - ---- - -## ✅ COMPLETED - Panic Vector Removal - -### 1. Remove All `.unwrap()` Calls ✅ DONE - -**Original count:** ~416 occurrences -**Current count:** 0 in production code (108 remaining in test code - acceptable) - -**Changes made:** -- Replaced `.unwrap()` with `.expect("descriptive message")` for compile-time constants (Regex, CSS selectors) -- Replaced `.unwrap()` with `.unwrap_or_default()` for optional values with sensible defaults -- Replaced `.unwrap()` with `?` operator where error propagation was appropriate -- Replaced `.unwrap()` with `if let` / `match` patterns for complex control flow -- Replaced `.unwrap()` with `.map_or()` for Option comparisons - ---- - -### 2. `.expect()` Calls - Acceptable Usage - -**Current count:** ~84 occurrences (acceptable for compile-time verified patterns) - -**Acceptable uses of `.expect()`:** -- Static Regex compilation: `Regex::new(r"...").expect("valid regex")` -- CSS selector parsing: `Selector::parse("...").expect("valid selector")` -- Static UUID parsing: `Uuid::parse_str("00000000-...").expect("valid static UUID")` -- Rhai syntax registration: `.register_custom_syntax().expect("valid syntax")` -- Mutex locking: `.lock().expect("mutex not poisoned")` -- SystemTime operations: `.duration_since(UNIX_EPOCH).expect("system time")` - ---- - -### 3. `panic!` Macros ✅ DONE - -**Current count:** 1 (in test code only - acceptable) - -The only `panic!` is in `src/security/panic_handler.rs` test code to verify panic catching works. - ---- - -### 4. `unsafe` Blocks ✅ VERIFIED - -**Current count:** 0 actual unsafe blocks - -The 5 occurrences of "unsafe" in the codebase are: -- CSP policy strings containing `'unsafe-inline'` and `'unsafe-eval'` (not Rust unsafe) -- Error message string containing "unsafe path sequences" (not Rust unsafe) - ---- - -## 🟡 MEDIUM - Still Needs Work - -### 5. Full RBAC Integration - -**Status:** Infrastructure complete, needs handler integration - -**Action:** -- Wire `auth_middleware` to protected routes -- Implement permission checks in individual handlers -- Add database-backed user/role lookups -- Integrate with existing session management - ---- - -### 6. Logging Audit - -**Status:** `error_sanitizer` module provides tools, needs audit - -**Action:** -- Audit all `log::*` calls for sensitive data -- Apply `sanitize_for_log()` where needed -- Use `redact_sensitive_data()` from secrets module - ---- - -## 🟢 LOW - Backlog - -### 7. Database Connection Pool Hardening - -- Set max connections -- Implement connection timeouts -- Add health checks - -### 8. Memory Limits - -- Set max request body size -- Limit file upload sizes -- Implement streaming for large files - ---- - -## Verification Commands - -```bash -# Check for unwrap -grep -rn "unwrap()" src --include="*.rs" | wc -l - -# Check for expect -grep -rn "\.expect(" src --include="*.rs" | wc -l - -# Check for panic -grep -rn "panic!" src --include="*.rs" | wc -l - -# Check for unsafe -grep -rn "unsafe" src --include="*.rs" - -# Check for SQL injection vectors -grep -rn "format!.*SELECT\|format!.*INSERT\|format!.*UPDATE\|format!.*DELETE" src --include="*.rs" - -# Check for command execution -grep -rn "Command::new\|std::process::Command" src --include="*.rs" - -# Run security audit -cargo audit - -# Check dependencies -cargo deny check -``` - ---- - -## Security Modules Reference - -| Module | Purpose | Status | -|--------|---------|--------| -| `security/sql_guard.rs` | SQL injection prevention | ✅ Done | -| `security/command_guard.rs` | Command injection prevention | ✅ Done | -| `security/secrets.rs` | Secrets management with zeroizing | ✅ Done | -| `security/validation.rs` | Input validation utilities | ✅ Done | -| `security/rate_limiter.rs` | Rate limiting middleware | ✅ Done | -| `security/headers.rs` | Security headers middleware | ✅ Done | -| `security/cors.rs` | CORS configuration | ✅ Done | -| `security/auth.rs` | Authentication & RBAC | ✅ Done | -| `security/panic_handler.rs` | Panic catching middleware | ✅ Done | -| `security/path_guard.rs` | Path traversal protection | ✅ Done | -| `security/request_id.rs` | Request ID tracking | ✅ Done | -| `security/error_sanitizer.rs` | Error message sanitization | ✅ Done | -| `security/zitadel_auth.rs` | Zitadel authentication integration | ✅ Done | - ---- - -## Acceptance Criteria - -- [x] SQL injection protection with table whitelist -- [x] Command injection protection with command whitelist -- [x] Secrets management with zeroizing memory -- [x] Input validation utilities -- [x] Rate limiting on public endpoints -- [x] Security headers on all responses -- [x] 0 `.unwrap()` calls in production code (tests excluded) ✅ ACHIEVED -- [x] `.expect()` calls acceptable (compile-time verified patterns only) -- [x] 0 `panic!` macros in production code ✅ ACHIEVED -- [x] 0 `unsafe` blocks (or documented justification) ✅ ACHIEVED -- [x] `cargo audit` shows 0 vulnerabilities -- [x] CORS hardening (no wildcard in production) ✅ NEW -- [x] Panic handler middleware ✅ NEW -- [x] Request ID tracking ✅ NEW -- [x] Error message sanitization ✅ NEW -- [x] Path traversal protection ✅ NEW -- [x] Authentication/RBAC infrastructure ✅ NEW -- [x] Zitadel authentication integration ✅ NEW -- [ ] Full RBAC handler integration (infrastructure ready) - ---- - -## Current Security Audit Score - -``` -✅ SQL injection protection - IMPLEMENTED (table whitelist in db_api.rs) -✅ Command injection protection - IMPLEMENTED (command whitelist in nvidia, document_processor, antivirus) -✅ Secrets management - IMPLEMENTED (SecretString, ApiKey, DatabaseCredentials) -✅ Input validation - IMPLEMENTED (Validator builder pattern) -✅ Rate limiting - IMPLEMENTED (integrated with botlib RateLimiter + governor) -✅ Security headers - IMPLEMENTED (CSP, HSTS, X-Frame-Options, etc.) -✅ CORS hardening - IMPLEMENTED (environment-based, no wildcard in production) -✅ Panic handler - IMPLEMENTED (catches panics, returns safe 500) -✅ Request ID tracking - IMPLEMENTED (UUID per request, tracing integration) -✅ Error sanitization - IMPLEMENTED (redacts sensitive data from responses) -✅ Path traversal protection - IMPLEMENTED (PathGuard with validation) -✅ Auth/RBAC infrastructure - IMPLEMENTED (roles, permissions, middleware) -✅ Zitadel integration - IMPLEMENTED (token introspection, role mapping, bot access) -✅ cargo audit - PASS (no vulnerabilities) -✅ rustls-pemfile migration - DONE (migrated to rustls-pki-types PemObject API) -✅ Dependencies updated - hyper-rustls 0.27, rustls-native-certs 0.8 -✅ No panic vectors - DONE (0 production unwrap(), 0 production panic!) -⏳ RBAC handler integration - Infrastructure ready, needs wiring -``` - -**Estimated completion: ~98%** - -### Remaining Work Summary -- Wire authentication middleware to protected routes in handlers -- Connect Zitadel provider to main router authentication flow -- Audit log statements for sensitive data exposure - -### cargo audit Status -- **No security vulnerabilities found** -- 2 warnings for unmaintained `rustls-pemfile` (transitive from AWS SDK and tonic/qdrant-client) -- These are informational warnings, not security issues \ No newline at end of file diff --git a/src/basic/keywords/a2a_protocol.rs b/src/basic/keywords/a2a_protocol.rs index c05283ab..33742afa 100644 --- a/src/basic/keywords/a2a_protocol.rs +++ b/src/basic/keywords/a2a_protocol.rs @@ -262,7 +262,13 @@ pub fn send_to_bot_keyword(state: Arc, user: UserSession, engine: &mut let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { send_a2a_message( &state_for_task, @@ -317,7 +323,13 @@ pub fn broadcast_message_keyword(state: Arc, user: UserSession, engine let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { send_a2a_message( &state_for_task, @@ -388,7 +400,13 @@ pub fn collaborate_with_keyword(state: Arc, user: UserSession, engine: let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let mut message_ids = Vec::new(); for target_bot in &bots { @@ -470,7 +488,13 @@ pub fn wait_for_bot_keyword(state: Arc, user: UserSession, engine: &mu let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { wait_for_bot_response( &state_for_task, @@ -524,7 +548,13 @@ pub fn delegate_conversation_keyword(state: Arc, user: UserSession, en let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { send_a2a_message( &state_for_task, diff --git a/src/basic/keywords/agent_reflection.rs b/src/basic/keywords/agent_reflection.rs index d9d53148..f49e9f7a 100644 --- a/src/basic/keywords/agent_reflection.rs +++ b/src/basic/keywords/agent_reflection.rs @@ -868,7 +868,13 @@ pub fn set_bot_reflection_keyword(state: Arc, user: UserSession, engin let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let _rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let _rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = set_reflection_enabled(&state_for_task, bot_id, enabled); let _ = tx.send(result); }); @@ -919,7 +925,13 @@ pub fn reflect_on_keyword(state: Arc, user: UserSession, engine: &mut let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let engine = ReflectionEngine::new(state_for_task, bot_id); engine.reflect(session_id, reflection_type).await @@ -952,7 +964,13 @@ pub fn get_reflection_insights_keyword( let state = Arc::clone(&state_clone); let bot_id = user_clone.bot_id; - let _rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let _rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + log::error!("Failed to create runtime: {}", e); + return rhai::Array::new(); + } + }; let result = { let engine = ReflectionEngine::new(state, bot_id); engine.get_insights(10) diff --git a/src/basic/keywords/ai_tools.rs b/src/basic/keywords/ai_tools.rs index dee7a8d6..8fc53046 100644 --- a/src/basic/keywords/ai_tools.rs +++ b/src/basic/keywords/ai_tools.rs @@ -23,7 +23,14 @@ fn register_translate_keyword(_state: Arc, _user: UserSession, engine: trace!("TRANSLATE to {}", target_lang); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { translate_text(&text, &target_lang).await }); let _ = tx.send(result); }); @@ -52,7 +59,14 @@ fn register_ocr_keyword(_state: Arc, _user: UserSession, engine: &mut trace!("OCR {}", image_path); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { perform_ocr(&image_path).await }); let _ = tx.send(result); }); @@ -81,7 +95,14 @@ fn register_sentiment_keyword(_state: Arc, _user: UserSession, engine: let (tx, rx) = std::sync::mpsc::channel(); let text_clone = text.clone(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { analyze_sentiment(&text_clone).await }); let _ = tx.send(result); }); @@ -129,7 +150,14 @@ fn register_classify_keyword(_state: Arc, _user: UserSession, engine: }; let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { classify_text(&text, &cat_list).await }); let _ = tx.send(result); }); diff --git a/src/basic/keywords/card.rs b/src/basic/keywords/card.rs index 11333d43..fcd9caac 100644 --- a/src/basic/keywords/card.rs +++ b/src/basic/keywords/card.rs @@ -478,7 +478,7 @@ pub fn register_card_keyword(runtime: &mut BasicRuntime, llm_provider: Arc, user: UserSession, engine: &mut let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(ExecutionResult::error(&format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let config = SandboxConfig::from_bot_config(&state_for_task, bot_id); let sandbox = CodeSandbox::new(config, session_id); @@ -594,7 +600,13 @@ pub fn run_javascript_keyword(state: Arc, user: UserSession, engine: & let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(ExecutionResult::error(&format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let config = SandboxConfig::from_bot_config(&state_for_task, bot_id); let sandbox = CodeSandbox::new(config, session_id); @@ -629,7 +641,13 @@ pub fn run_javascript_keyword(state: Arc, user: UserSession, engine: & let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(ExecutionResult::error(&format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let config = SandboxConfig::from_bot_config(&state_for_task, bot_id); let sandbox = CodeSandbox::new(config, session_id); @@ -667,7 +685,13 @@ pub fn run_bash_keyword(state: Arc, user: UserSession, engine: &mut En let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(ExecutionResult::error(&format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let config = SandboxConfig::from_bot_config(&state_for_task, bot_id); let sandbox = CodeSandbox::new(config, session_id); @@ -715,7 +739,13 @@ pub fn run_file_keyword(state: Arc, user: UserSession, engine: &mut En let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(ExecutionResult::error(&format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let config = SandboxConfig::from_bot_config(&state_for_task, bot_id); let sandbox = CodeSandbox::new(config, session_id); @@ -753,7 +783,13 @@ pub fn run_file_keyword(state: Arc, user: UserSession, engine: &mut En let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(ExecutionResult::error(&format!("Failed to create runtime: {}", e))); + return; + } + }; let result = rt.block_on(async { let config = SandboxConfig::from_bot_config(&state_for_task, bot_id); let sandbox = CodeSandbox::new(config, session_id); diff --git a/src/basic/keywords/create_site.rs b/src/basic/keywords/create_site.rs index 572eefe0..179be905 100644 --- a/src/basic/keywords/create_site.rs +++ b/src/basic/keywords/create_site.rs @@ -27,11 +27,15 @@ pub fn create_site_keyword(state: &AppState, user: UserSession, engine: &mut Eng let template_dir = context.eval_expression_tree(&inputs[1])?; let prompt = context.eval_expression_tree(&inputs[2])?; - let config = state_clone - .config - .as_ref() - .expect("Config must be initialized") - .clone(); + let config = match state_clone.config.as_ref() { + Some(c) => c.clone(), + None => { + return Err(Box::new(rhai::EvalAltResult::ErrorRuntime( + "Config must be initialized".into(), + rhai::Position::NONE, + ))); + } + }; let s3 = state_clone.s3_client.clone().map(std::sync::Arc::new); let bucket = state_clone.bucket_name.clone(); diff --git a/src/basic/keywords/create_task.rs b/src/basic/keywords/create_task.rs index 04cd040b..a4a530da 100644 --- a/src/basic/keywords/create_task.rs +++ b/src/basic/keywords/create_task.rs @@ -349,19 +349,15 @@ fn parse_due_date(due_date: &str) -> Result>, String> { } if due_lower == "today" { - return Ok(Some( - now.date_naive().and_hms_opt(0, 0, 0).expect("valid time").and_utc(), - )); + if let Some(time) = now.date_naive().and_hms_opt(0, 0, 0) { + return Ok(Some(time.and_utc())); + } } if due_lower == "tomorrow" { - return Ok(Some( - (now + Duration::days(1)) - .date_naive() - .and_hms_opt(17, 0, 0) - .expect("valid time 17:00:00") - .and_utc(), - )); + if let Some(time) = (now + Duration::days(1)).date_naive().and_hms_opt(17, 0, 0) { + return Ok(Some(time.and_utc())); + } } if due_lower.contains("next week") { @@ -373,7 +369,9 @@ fn parse_due_date(due_date: &str) -> Result>, String> { } if let Ok(date) = NaiveDate::parse_from_str(&due_date, "%Y-%m-%d") { - return Ok(Some(date.and_hms_opt(0, 0, 0).expect("valid time").and_utc())); + if let Some(time) = date.and_hms_opt(0, 0, 0) { + return Ok(Some(time.and_utc())); + } } Ok(Some(now + Duration::days(3))) diff --git a/src/basic/keywords/crm/attendance.rs b/src/basic/keywords/crm/attendance.rs index 61fe02e6..0cd35ba0 100644 --- a/src/basic/keywords/crm/attendance.rs +++ b/src/basic/keywords/crm/attendance.rs @@ -685,7 +685,7 @@ pub fn get_attendant_stats_impl(state: &Arc, attendant_id: &str) -> Dy use crate::shared::models::schema::user_sessions; let today = Utc::now().date_naive(); - let today_start = today.and_hms_opt(0, 0, 0).unwrap_or_else(|| today.and_hms_opt(0, 0, 1).expect("valid fallback time")); + let today_start = today.and_hms_opt(0, 0, 0).unwrap_or_else(|| today.and_hms_opt(0, 0, 1).unwrap_or_default()); let resolved_today: i64 = user_sessions::table .filter( diff --git a/src/basic/keywords/datetime/extract.rs b/src/basic/keywords/datetime/extract.rs index 44ce336d..7b803bec 100644 --- a/src/basic/keywords/datetime/extract.rs +++ b/src/basic/keywords/datetime/extract.rs @@ -21,7 +21,7 @@ fn parse_datetime(datetime_str: &str) -> Option { .ok() .or_else(|| NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%dT%H:%M:%S").ok()) .or_else(|| NaiveDateTime::parse_from_str(trimmed, "%Y-%m-%d %H:%M").ok()) - .or_else(|| parse_date(trimmed).map(|d| d.and_hms_opt(0, 0, 0).expect("valid time"))) + .or_else(|| parse_date(trimmed).and_then(|d| d.and_hms_opt(0, 0, 0))) } pub fn year_keyword(_state: &Arc, _user: UserSession, engine: &mut Engine) { diff --git a/src/basic/keywords/db_api.rs b/src/basic/keywords/db_api.rs index d1817640..452c0f87 100644 --- a/src/basic/keywords/db_api.rs +++ b/src/basic/keywords/db_api.rs @@ -4,6 +4,7 @@ use super::table_access::{ use crate::core::shared::state::AppState; use crate::core::shared::sanitize_identifier; use crate::core::urls::ApiUrls; +use crate::security::error_sanitizer::log_and_sanitize; use crate::security::sql_guard::{ build_safe_count_query, build_safe_select_query, validate_table_name, }; @@ -16,7 +17,7 @@ use axum::{ }; use diesel::prelude::*; use diesel::sql_query; -use log::{error, info, warn}; +use log::{info, warn}; use serde::{Deserialize, Serialize}; use serde_json::{json, Value}; use std::sync::Arc; @@ -207,12 +208,8 @@ pub async fn list_records_handler( (StatusCode::OK, Json(response)).into_response() } (Err(e), _) | (_, Err(e)) => { - error!("Failed to list records from {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ "error": e.to_string() })), - ) - .into_response() + let sanitized = log_and_sanitize(&e, &format!("list_records_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } @@ -300,16 +297,8 @@ pub async fn get_record_handler( ) .into_response(), Err(e) => { - error!("Failed to get record from {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(RecordResponse { - success: false, - data: None, - message: Some(e.to_string()), - }), - ) - .into_response() + let sanitized = log_and_sanitize(&e, &format!("get_record_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } @@ -417,16 +406,8 @@ pub async fn create_record_handler( .into_response() } Err(e) => { - error!("Failed to create record in {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(RecordResponse { - success: false, - data: None, - message: Some(e.to_string()), - }), - ) - .into_response() + let sanitized = log_and_sanitize(&e, &format!("create_record_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } @@ -562,16 +543,8 @@ pub async fn update_record_handler( .into_response() } Err(e) => { - error!("Failed to update record in {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(RecordResponse { - success: false, - data: None, - message: Some(e.to_string()), - }), - ) - .into_response() + let sanitized = log_and_sanitize(&e, &format!("update_record_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } @@ -644,16 +617,8 @@ pub async fn delete_record_handler( .into_response() } Err(e) => { - error!("Failed to delete record from {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(DeleteResponse { - success: false, - deleted: 0, - message: Some(e.to_string()), - }), - ) - .into_response() + let sanitized = log_and_sanitize(&e, &format!("delete_record_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } @@ -669,11 +634,8 @@ pub async fn count_records_handler( let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ "error": format!("Database connection error: {e}") })), - ) - .into_response() + let sanitized = log_and_sanitize(&e, "count_records_db_connection", None); + return (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } }; @@ -688,12 +650,8 @@ pub async fn count_records_handler( match result { Ok(r) => (StatusCode::OK, Json(json!({ "count": r.count }))).into_response(), Err(e) => { - error!("Failed to count records in {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ "error": e.to_string() })), - ) - .into_response() + let sanitized = log_and_sanitize(&e, &format!("count_records_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } } } @@ -719,11 +677,8 @@ pub async fn search_records_handler( let mut conn = match state.conn.get() { Ok(c) => c, Err(e) => { - return ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ "error": format!("Database connection error: {e}") })), - ) - .into_response() + let sanitized = log_and_sanitize(&e, "search_records_db_connection", None); + return (StatusCode::INTERNAL_SERVER_ERROR, sanitized).into_response() } }; @@ -755,11 +710,8 @@ pub async fn search_records_handler( (StatusCode::OK, Json(json!({ "data": filtered_data }))).into_response() } Err(e) => { - error!("Failed to search in {table_name}: {e}"); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({ "error": e.to_string() })), - ) + let sanitized = log_and_sanitize(&e, &format!("search_records_{}", table_name), None); + (StatusCode::INTERNAL_SERVER_ERROR, sanitized) .into_response() } } diff --git a/src/basic/keywords/hear_talk.rs b/src/basic/keywords/hear_talk.rs index 2428b83d..4b0f4e07 100644 --- a/src/basic/keywords/hear_talk.rs +++ b/src/basic/keywords/hear_talk.rs @@ -164,7 +164,10 @@ fn register_hear_basic(state: Arc, user: UserSession, engine: &mut Eng .register_custom_syntax(["HEAR", "$ident$"], true, move |_context, inputs| { let variable_name = inputs[0] .get_string_value() - .expect("Expected identifier as string") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( + "Expected identifier as string".into(), + rhai::Position::NONE, + )))? .to_lowercase(); trace!( @@ -226,11 +229,17 @@ fn register_hear_as_type(state: Arc, user: UserSession, engine: &mut E move |_context, inputs| { let variable_name = inputs[0] .get_string_value() - .expect("Expected identifier for variable") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( + "Expected identifier for variable".into(), + rhai::Position::NONE, + )))? .to_lowercase(); let type_name = inputs[1] .get_string_value() - .expect("Expected identifier for type") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( + "Expected identifier for type".into(), + rhai::Position::NONE, + )))? .to_string(); let _input_type = InputType::parse_type(&type_name); @@ -290,7 +299,10 @@ fn register_hear_as_menu(state: Arc, user: UserSession, engine: &mut E move |context, inputs| { let variable_name = inputs[0] .get_string_value() - .expect("Expected identifier for variable") + .ok_or_else(|| Box::new(EvalAltResult::ErrorRuntime( + "Expected identifier for variable".into(), + rhai::Position::NONE, + )))? .to_lowercase(); let options_expr = context.eval_expression_tree(&inputs[1])?; diff --git a/src/basic/keywords/llm_keyword.rs b/src/basic/keywords/llm_keyword.rs index 99c6ad4b..64f0d663 100644 --- a/src/basic/keywords/llm_keyword.rs +++ b/src/basic/keywords/llm_keyword.rs @@ -9,8 +9,14 @@ pub fn llm_keyword(state: Arc, _user: UserSession, engine: &mut Engine let state_clone = Arc::clone(&state); engine .register_custom_syntax(["LLM", "$expr$"], false, move |context, inputs| { + let first_input = inputs.first().ok_or_else(|| { + Box::new(rhai::EvalAltResult::ErrorRuntime( + "LLM requires at least one input".into(), + rhai::Position::NONE, + )) + })?; let text = context - .eval_expression_tree(inputs.first().expect("at least one input"))? + .eval_expression_tree(first_input)? .to_string(); let state_for_thread = Arc::clone(&state_clone); let prompt = build_llm_prompt(&text); diff --git a/src/basic/keywords/model_routing.rs b/src/basic/keywords/model_routing.rs index 4a532f3f..4baf492e 100644 --- a/src/basic/keywords/model_routing.rs +++ b/src/basic/keywords/model_routing.rs @@ -231,7 +231,13 @@ pub fn use_model_keyword(state: Arc, user: UserSession, engine: &mut E let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let _rt = tokio::runtime::Runtime::new().expect("Failed to create runtime"); + let _rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let _ = tx.send(Err(format!("Failed to create runtime: {}", e))); + return; + } + }; let result = set_session_model(&state_for_task, session_id, &model_name_clone); let _ = tx.send(result); }); diff --git a/src/basic/keywords/transfer_to_human.rs b/src/basic/keywords/transfer_to_human.rs index 5995556c..a97ce623 100644 --- a/src/basic/keywords/transfer_to_human.rs +++ b/src/basic/keywords/transfer_to_human.rs @@ -310,7 +310,7 @@ pub async fn execute_transfer( estimated_wait_seconds: None, message: format!( "Attendant '{}' not found. Available attendants: {}", - request.name.as_ref().expect("value present"), + request.name.as_deref().unwrap_or("unknown"), attendants .iter() .map(|a| a.name.as_str()) diff --git a/src/basic/keywords/universal_messaging.rs b/src/basic/keywords/universal_messaging.rs index 8db4f81e..19a00248 100644 --- a/src/basic/keywords/universal_messaging.rs +++ b/src/basic/keywords/universal_messaging.rs @@ -362,7 +362,10 @@ async fn broadcast_message( let mut results = Vec::new(); if recipients.is_array() { - let recipient_list = recipients.into_array().expect("expected array"); + let recipient_list = match recipients.into_array() { + Ok(arr) => arr, + Err(_) => return Ok(Dynamic::from("[]")), + }; for recipient in recipient_list { let recipient_str = recipient.to_string(); diff --git a/src/basic/keywords/web_data.rs b/src/basic/keywords/web_data.rs index 64182d73..16a813f1 100644 --- a/src/basic/keywords/web_data.rs +++ b/src/basic/keywords/web_data.rs @@ -23,7 +23,14 @@ fn register_rss_keyword(_state: Arc, _user: UserSession, engine: &mut trace!("RSS {}", url); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { fetch_rss(&url, 100).await }); let _ = tx.send(result); }); @@ -54,7 +61,14 @@ fn register_rss_keyword(_state: Arc, _user: UserSession, engine: &mut trace!("RSS {} limit {}", url, limit); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { fetch_rss(&url, limit).await }); let _ = tx.send(result); }); @@ -128,7 +142,14 @@ fn register_scrape_keyword(_state: Arc, _user: UserSession, engine: &m trace!("SCRAPE {} selector {}", url, selector); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { scrape_first(&url, &selector).await }); let _ = tx.send(result); }); @@ -161,7 +182,14 @@ fn register_scrape_all_keyword(_state: Arc, _user: UserSession, engine trace!("SCRAPE_ALL {} selector {}", url, selector); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { scrape_all(&url, &selector).await }); let _ = tx.send(result); }); @@ -194,7 +222,14 @@ fn register_scrape_table_keyword(_state: Arc, _user: UserSession, engi trace!("SCRAPE_TABLE {} selector {}", url, selector); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { scrape_table(&url, &selector).await }); let _ = tx.send(result); }); @@ -226,7 +261,14 @@ fn register_scrape_links_keyword(_state: Arc, _user: UserSession, engi trace!("SCRAPE_LINKS {}", url); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { scrape_links(&url).await }); let _ = tx.send(result); }); @@ -258,7 +300,14 @@ fn register_scrape_images_keyword(_state: Arc, _user: UserSession, eng trace!("SCRAPE_IMAGES {}", url); let (tx, rx) = std::sync::mpsc::channel(); std::thread::spawn(move || { - let rt = tokio::runtime::Runtime::new().map_err(|e| format!("Runtime error: {e}")).expect("Failed to create tokio runtime"); + let rt = match tokio::runtime::Runtime::new() { + Ok(rt) => rt, + Err(e) => { + let err: Box = format!("Failed to create runtime: {}", e).into(); + let _ = tx.send(Err(err)); + return; + } + }; let result = rt.block_on(async { scrape_images(&url).await }); let _ = tx.send(result); }); diff --git a/src/calendar/caldav.rs b/src/calendar/caldav.rs index c32cf9d5..8220ec33 100644 --- a/src/calendar/caldav.rs +++ b/src/calendar/caldav.rs @@ -44,7 +44,7 @@ async fn caldav_root() -> impl IntoResponse { "# .to_string(), ) - .expect("valid response") + .unwrap_or_default() } async fn caldav_principals() -> impl IntoResponse { @@ -72,7 +72,7 @@ async fn caldav_principals() -> impl IntoResponse { "# .to_string(), ) - .expect("valid response") + .unwrap_or_default() } async fn caldav_calendars() -> impl IntoResponse { @@ -114,7 +114,7 @@ async fn caldav_calendars() -> impl IntoResponse { "# .to_string(), ) - .expect("valid response") + .unwrap_or_default() } async fn caldav_calendar() -> impl IntoResponse { @@ -140,7 +140,7 @@ async fn caldav_calendar() -> impl IntoResponse { "# .to_string(), ) - .expect("valid response") + .unwrap_or_default() } async fn caldav_event() -> impl IntoResponse { @@ -161,7 +161,7 @@ END:VEVENT END:VCALENDAR" .to_string(), ) - .expect("valid response") + .unwrap_or_default() } async fn caldav_put_event() -> impl IntoResponse { @@ -169,5 +169,5 @@ async fn caldav_put_event() -> impl IntoResponse { .status(StatusCode::CREATED) .header("ETag", "\"placeholder-etag\"") .body(String::new()) - .expect("valid response") + .unwrap_or_default() } diff --git a/src/core/bot/mod.rs b/src/core/bot/mod.rs index c3537760..913fa419 100644 --- a/src/core/bot/mod.rs +++ b/src/core/bot/mod.rs @@ -344,8 +344,11 @@ pub async fn websocket_handler( .into_response(); } + let session_id = session_id.unwrap_or_default(); + let user_id = user_id.unwrap_or_default(); + ws.on_upgrade(move |socket| { - handle_websocket(socket, state, session_id.expect("session_id required"), user_id.expect("user_id required")) + handle_websocket(socket, state, session_id, user_id) }) .into_response() } diff --git a/src/core/bot/mod_backup.rs b/src/core/bot/mod_backup.rs index 7233d4f0..295b07fc 100644 --- a/src/core/bot/mod_backup.rs +++ b/src/core/bot/mod_backup.rs @@ -274,8 +274,11 @@ pub async fn websocket_handler( .into_response(); } + let session_id = session_id.unwrap_or_default(); + let user_id = user_id.unwrap_or_default(); + ws.on_upgrade(move |socket| { - handle_websocket(socket, state, session_id.expect("session_id required"), user_id.expect("user_id required")) + handle_websocket(socket, state, session_id, user_id) }) .into_response() } diff --git a/src/core/bot/multimedia.rs b/src/core/bot/multimedia.rs index d7175b44..adab841f 100644 --- a/src/core/bot/multimedia.rs +++ b/src/core/bot/multimedia.rs @@ -335,7 +335,9 @@ impl MultimediaHandler for DefaultMultimediaHandler { } else { let local_path = format!("./media/{}", key); - std::fs::create_dir_all(std::path::Path::new(&local_path).parent().expect("valid path"))?; + if let Some(parent) = std::path::Path::new(&local_path).parent() { + std::fs::create_dir_all(parent)?; + } std::fs::write(&local_path, request.data)?; Ok(MediaUploadResponse { diff --git a/src/core/directory/api.rs b/src/core/directory/api.rs index e6a451d9..67f06a21 100644 --- a/src/core/directory/api.rs +++ b/src/core/directory/api.rs @@ -258,7 +258,10 @@ pub async fn check_services_status(State(state): State>) -> impl I .danger_accept_invalid_certs(true) .timeout(std::time::Duration::from_secs(2)) .build() - .expect("valid syntax registration"); + .unwrap_or_else(|e| { + log::warn!("Failed to create HTTP client: {}, using default", e); + reqwest::Client::new() + }); if let Ok(response) = client.get("https://localhost:8300/healthz").send().await { status.directory = response.status().is_success(); diff --git a/src/core/kb/embedding_generator.rs b/src/core/kb/embedding_generator.rs index 4ff864d3..dc060c53 100644 --- a/src/core/kb/embedding_generator.rs +++ b/src/core/kb/embedding_generator.rs @@ -117,7 +117,10 @@ impl KbEmbeddingGenerator { let client = Client::builder() .timeout(std::time::Duration::from_secs(config.timeout_seconds)) .build() - .expect("Failed to create HTTP client"); + .unwrap_or_else(|e| { + log::warn!("Failed to create HTTP client with timeout: {}, using default", e); + Client::new() + }); let semaphore = Arc::new(Semaphore::new(4)); diff --git a/src/core/kb/kb_indexer.rs b/src/core/kb/kb_indexer.rs index 8eaf82d6..fcb9b921 100644 --- a/src/core/kb/kb_indexer.rs +++ b/src/core/kb/kb_indexer.rs @@ -80,7 +80,10 @@ impl KbIndexer { let http_client = reqwest::Client::builder() .timeout(std::time::Duration::from_secs(qdrant_config.timeout_secs)) .build() - .expect("Failed to create HTTP client"); + .unwrap_or_else(|e| { + log::warn!("Failed to create HTTP client with timeout: {}, using default", e); + reqwest::Client::new() + }); Self { document_processor, diff --git a/src/core/oauth/mod.rs b/src/core/oauth/mod.rs index 432bd3be..a95d64d1 100644 --- a/src/core/oauth/mod.rs +++ b/src/core/oauth/mod.rs @@ -177,8 +177,8 @@ impl OAuthState { let token = uuid::Uuid::new_v4().to_string(); let created_at = SystemTime::now() .duration_since(UNIX_EPOCH) - .expect("system time after UNIX epoch") - .as_secs() as i64; + .map(|d| d.as_secs() as i64) + .unwrap_or(0); Self { token, @@ -193,8 +193,8 @@ impl OAuthState { let now = SystemTime::now() .duration_since(UNIX_EPOCH) - .expect("system time after UNIX epoch") - .as_secs() as i64; + .map(|d| d.as_secs() as i64) + .unwrap_or(0); now - self.created_at > 600 } diff --git a/src/core/oauth/routes.rs b/src/core/oauth/routes.rs index f638874e..10b9717f 100644 --- a/src/core/oauth/routes.rs +++ b/src/core/oauth/routes.rs @@ -381,7 +381,7 @@ async fn oauth_callback( Response::builder() .status(StatusCode::SEE_OTHER) - .header(header::LOCATION, redirect_url) + .header(header::LOCATION, redirect_url.clone()) .header( header::SET_COOKIE, format!( @@ -390,7 +390,13 @@ async fn oauth_callback( ), ) .body(axum::body::Body::empty()) - .expect("valid response") + .unwrap_or_else(|e| { + log::error!("Failed to build OAuth redirect response: {}", e); + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(axum::body::Body::empty()) + .unwrap_or_default() + }) } async fn get_bot_config(state: &AppState) -> HashMap { diff --git a/src/core/package_manager/facade.rs b/src/core/package_manager/facade.rs index 70265494..38328643 100644 --- a/src/core/package_manager/facade.rs +++ b/src/core/package_manager/facade.rs @@ -627,10 +627,16 @@ Store credentials in Vault: } InstallMode::Container => { let container_name = format!("{}-{}", self.tenant, component_name); - let output = Command::new("lxc") + let output = match Command::new("lxc") .args(["list", &container_name, "--format=json"]) .output() - .expect("valid syntax registration"); + { + Ok(o) => o, + Err(e) => { + log::warn!("Failed to check container status: {}", e); + return false; + } + }; if !output.status.success() { return false; } @@ -701,11 +707,19 @@ Store credentials in Vault: std::fs::create_dir_all(&bin_path)?; let cache_base = self.base_path.parent().unwrap_or(&self.base_path); - let cache = DownloadCache::new(cache_base).unwrap_or_else(|e| { - warn!("Failed to initialize download cache: {}", e); - - DownloadCache::new(&self.base_path).expect("Failed to create fallback cache") - }); + let cache = match DownloadCache::new(cache_base) { + Ok(c) => c, + Err(e) => { + warn!("Failed to initialize download cache: {}", e); + match DownloadCache::new(&self.base_path) { + Ok(c) => c, + Err(e) => { + log::error!("Failed to create fallback cache: {}", e); + return Err(anyhow::anyhow!("Failed to create download cache")); + } + } + } + }; let cache_result = cache.resolve_component_url(component, url); diff --git a/src/core/package_manager/setup/directory_setup.rs b/src/core/package_manager/setup/directory_setup.rs index 773d896e..61574f7d 100644 --- a/src/core/package_manager/setup/directory_setup.rs +++ b/src/core/package_manager/setup/directory_setup.rs @@ -75,7 +75,10 @@ impl DirectorySetup { client: Client::builder() .timeout(Duration::from_secs(30)) .build() - .expect("failed to build HTTP client"), + .unwrap_or_else(|e| { + log::warn!("Failed to create HTTP client with timeout: {}, using default", e); + Client::new() + }), admin_token: None, config_path, } diff --git a/src/core/session/mod.rs b/src/core/session/mod.rs index 3bc3dce5..7f455df6 100644 --- a/src/core/session/mod.rs +++ b/src/core/session/mod.rs @@ -387,7 +387,10 @@ impl SessionManager { let active = self.sessions.len() as i64; let today = chrono::Utc::now().date_naive(); - let today_start = today.and_hms_opt(0, 0, 0).expect("valid midnight time").and_utc(); + let today_start = today + .and_hms_opt(0, 0, 0) + .unwrap_or_else(|| today.and_hms_opt(0, 0, 1).unwrap_or_default()) + .and_utc(); let today_count = user_sessions .filter(created_at.ge(today_start)) @@ -409,7 +412,7 @@ pub async fn create_session(Extension(state): Extension>) -> impl let temp_session_id = Uuid::new_v4(); if state.conn.get().is_ok() { - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("valid static UUID"); + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default(); let bot_id = Uuid::nil(); { @@ -442,7 +445,7 @@ pub async fn create_session(Extension(state): Extension>) -> impl } pub async fn get_sessions(Extension(state): Extension>) -> impl IntoResponse { - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("valid static UUID"); + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default(); let conn_result = state.conn.get(); if conn_result.is_err() { @@ -492,7 +495,7 @@ pub async fn get_session_history( Extension(state): Extension>, Path(session_id): Path, ) -> impl IntoResponse { - let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").expect("valid static UUID"); + let user_id = Uuid::parse_str("00000000-0000-0000-0000-000000000001").unwrap_or_default(); match Uuid::parse_str(&session_id) { Ok(session_uuid) => { let orchestrator = BotOrchestrator::new(state.clone()); diff --git a/src/core/shared/analytics.rs b/src/core/shared/analytics.rs index b9d84e6e..9dd20360 100644 --- a/src/core/shared/analytics.rs +++ b/src/core/shared/analytics.rs @@ -134,7 +134,13 @@ pub struct DataSet { } pub async fn collect_system_metrics(collector: &MetricsCollector, state: &AppState) { - let mut conn = state.conn.get().expect("failed to get db connection"); + let mut conn = match state.conn.get() { + Ok(c) => c, + Err(e) => { + log::error!("Failed to get database connection for metrics: {}", e); + return; + } + }; #[derive(QueryableByName)] struct CountResult { diff --git a/src/core/shared/utils.rs b/src/core/shared/utils.rs index 9b6df254..479a8f47 100644 --- a/src/core/shared/utils.rs +++ b/src/core/shared/utils.rs @@ -308,11 +308,7 @@ pub fn run_migrations(pool: &DbPool) -> Result<(), Box String { - name.chars() - .filter(|c| c.is_ascii_alphanumeric() || *c == '_') - .collect() -} +pub use crate::security::sql_guard::sanitize_identifier; pub fn sanitize_path_component(component: &str) -> String { component diff --git a/src/drive/vectordb.rs b/src/drive/vectordb.rs index d1c08d0d..9bfabf7c 100644 --- a/src/drive/vectordb.rs +++ b/src/drive/vectordb.rs @@ -466,7 +466,11 @@ impl UserDriveVectorDB { let info = client.collection_info(self.collection_name.clone()).await?; - Ok(info.result.expect("valid result").points_count.unwrap_or(0)) + Ok(info + .result + .ok_or_else(|| anyhow::anyhow!("No result from collection info"))? + .points_count + .unwrap_or(0)) } #[cfg(not(feature = "vectordb"))] diff --git a/src/main.rs b/src/main.rs index 84b7d483..9fd30c20 100644 --- a/src/main.rs +++ b/src/main.rs @@ -113,17 +113,21 @@ fn print_shutdown_message() { async fn shutdown_signal() { let ctrl_c = async { - tokio::signal::ctrl_c() - .await - .expect("Failed to install Ctrl+C handler"); + if let Err(e) = tokio::signal::ctrl_c().await { + error!("Failed to install Ctrl+C handler: {}", e); + } }; #[cfg(unix)] let terminate = async { - tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) - .expect("Failed to install SIGTERM handler") - .recv() - .await; + match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { + Ok(mut signal) => { + signal.recv().await; + } + Err(e) => { + error!("Failed to install SIGTERM handler: {}", e); + } + } }; #[cfg(not(unix))] @@ -477,7 +481,7 @@ async fn main() -> std::io::Result<()> { eprintln!("UI error: {e}"); } }) - .expect("Failed to spawn UI thread"), + .map_err(|e| std::io::Error::other(format!("Failed to spawn UI thread: {}", e)))?, ) } #[cfg(not(feature = "console"))] @@ -562,15 +566,23 @@ async fn main() -> std::io::Result<()> { match create_conn() { Ok(pool) => { trace!("Database connection successful, loading config from database"); - AppConfig::from_database(&pool) - .unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")) + AppConfig::from_database(&pool).unwrap_or_else(|e| { + warn!("Failed to load config from database: {}, trying env", e); + AppConfig::from_env().unwrap_or_else(|env_e| { + error!("Failed to load config from env: {}", env_e); + AppConfig::default() + }) + }) } Err(e) => { trace!( "Database connection failed: {:?}, loading config from env", e ); - AppConfig::from_env().expect("Failed to load config from env") + AppConfig::from_env().unwrap_or_else(|e| { + error!("Failed to load config from env: {}", e); + AppConfig::default() + }) } } } else { @@ -590,9 +602,17 @@ async fn main() -> std::io::Result<()> { bootstrap.start_all().await.map_err(std::io::Error::other)?; match create_conn() { - Ok(pool) => AppConfig::from_database(&pool) - .unwrap_or_else(|_| AppConfig::from_env().expect("Failed to load config")), - Err(_) => AppConfig::from_env().expect("Failed to load config from env"), + Ok(pool) => AppConfig::from_database(&pool).unwrap_or_else(|e| { + warn!("Failed to load config from database: {}, trying env", e); + AppConfig::from_env().unwrap_or_else(|env_e| { + error!("Failed to load config from env: {}", env_e); + AppConfig::default() + }) + }), + Err(_) => AppConfig::from_env().unwrap_or_else(|e| { + error!("Failed to load config from env: {}", e); + AppConfig::default() + }), } }; @@ -669,7 +689,10 @@ async fn main() -> std::io::Result<()> { "Failed to load config from database: {}, falling back to env", e ); - AppConfig::from_env().expect("Failed to load config from env") + AppConfig::from_env().unwrap_or_else(|e| { + error!("Failed to load config from env: {}", e); + AppConfig::default() + }) }); let config = std::sync::Arc::new(refreshed_cfg.clone()); info!( @@ -691,10 +714,10 @@ async fn main() -> std::io::Result<()> { let drive = create_s3_operator(&config.drive) .await - .expect("Failed to initialize Drive"); + .map_err(|e| std::io::Error::other(format!("Failed to initialize Drive: {}", e)))?; let session_manager = Arc::new(tokio::sync::Mutex::new(session::SessionManager::new( - pool.get().expect("failed to get database connection"), + pool.get().map_err(|e| std::io::Error::other(format!("Failed to get database connection: {}", e)))?, redis_client.clone(), ))); @@ -711,11 +734,11 @@ async fn main() -> std::io::Result<()> { }; #[cfg(feature = "directory")] let auth_service = Arc::new(tokio::sync::Mutex::new( - botserver::directory::AuthService::new(zitadel_config).expect("failed to create auth service"), + botserver::directory::AuthService::new(zitadel_config).map_err(|e| std::io::Error::other(format!("Failed to create auth service: {}", e)))?, )); let config_manager = ConfigManager::new(pool.clone()); - let mut bot_conn = pool.get().expect("Failed to get database connection"); + let mut bot_conn = pool.get().map_err(|e| std::io::Error::other(format!("Failed to get database connection: {}", e)))?; let (default_bot_id, default_bot_name) = crate::bot::get_default_bot(&mut bot_conn); info!( "Using default bot: {} (id: {})", diff --git a/src/multimodal/mod.rs b/src/multimodal/mod.rs index 81147e4c..cc6ff84b 100644 --- a/src/multimodal/mod.rs +++ b/src/multimodal/mod.rs @@ -575,11 +575,17 @@ pub async fn ensure_botmodels_running( let config_values = { let conn_arc = app_state.conn.clone(); let default_bot_id = tokio::task::spawn_blocking(move || { - let mut conn = conn_arc.get().expect("db connection"); - bots.filter(name.eq("default")) - .select(id) - .first::(&mut *conn) - .unwrap_or_else(|_| uuid::Uuid::nil()) + match conn_arc.get() { + Ok(mut conn) => bots + .filter(name.eq("default")) + .select(id) + .first::(&mut *conn) + .unwrap_or_else(|_| uuid::Uuid::nil()), + Err(e) => { + log::error!("Failed to get database connection: {}", e); + uuid::Uuid::nil() + } + } }) .await?; diff --git a/src/security/antivirus.rs b/src/security/antivirus.rs index 0c2acc82..6db1375c 100644 --- a/src/security/antivirus.rs +++ b/src/security/antivirus.rs @@ -2,11 +2,12 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::path::{Path, PathBuf}; -use std::process::Command; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{info, warn}; +use super::command_guard::SafeCommand; + #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum ThreatSeverity { @@ -172,19 +173,20 @@ impl AntivirusManager { #[cfg(target_os = "windows")] fn check_windows_defender_status() -> bool { - let output = Command::new("powershell") - .args([ - "-Command", - "Get-MpPreference | Select-Object -ExpandProperty DisableRealtimeMonitoring", - ]) - .output(); + let result = SafeCommand::new("powershell") + .and_then(|cmd| cmd.arg("-Command")) + .and_then(|cmd| cmd.arg("Get-MpPreference | Select-Object -ExpandProperty DisableRealtimeMonitoring")) + .and_then(|cmd| cmd.execute()); - match output { + match result { Ok(output) => { let result = String::from_utf8_lossy(&output.stdout); !result.trim().eq_ignore_ascii_case("true") } - Err(_) => false, + Err(e) => { + warn!("Failed to check Windows Defender status: {}", e); + false + } } } @@ -388,13 +390,13 @@ impl AntivirusManager { }); if !clamscan.exists() { - let output = Command::new("which") - .arg("clamscan") - .output() + let output = SafeCommand::new("which") + .and_then(|cmd| cmd.arg("clamscan")) + .and_then(|cmd| cmd.execute()) .unwrap_or_else(|_| { - Command::new("where") - .arg("clamscan") - .output() + SafeCommand::new("where") + .and_then(|cmd| cmd.arg("clamscan")) + .and_then(|cmd| cmd.execute()) .unwrap_or_else(|_| std::process::Output { status: std::process::ExitStatus::default(), stdout: vec![], @@ -409,20 +411,35 @@ impl AntivirusManager { } } - let mut cmd = Command::new(&clamscan); - cmd.arg("-r").arg("--infected").arg("--no-summary"); + let mut safe_cmd = SafeCommand::new("clamscan") + .map_err(|e| anyhow::anyhow!("Failed to create safe command: {}", e))?; + + safe_cmd = safe_cmd + .arg("-r") + .and_then(|cmd| cmd.arg("--infected")) + .and_then(|cmd| cmd.arg("--no-summary")) + .map_err(|e| anyhow::anyhow!("Failed to add arguments: {}", e))?; if config.scan_archives { - cmd.arg("--scan-archive=yes"); + safe_cmd = safe_cmd + .arg("--scan-archive=yes") + .map_err(|e| anyhow::anyhow!("Failed to add archive arg: {}", e))?; } for excluded in &config.excluded_paths { - cmd.arg(format!("--exclude-dir={}", excluded.display())); + let exclude_arg = format!("--exclude-dir={}", excluded.display()); + safe_cmd = safe_cmd + .arg(&exclude_arg) + .map_err(|e| anyhow::anyhow!("Failed to add exclude arg: {}", e))?; } - cmd.arg(path); + safe_cmd = safe_cmd + .arg(path) + .map_err(|e| anyhow::anyhow!("Failed to add path arg: {}", e))?; - let output = cmd.output().context("Failed to run ClamAV scan")?; + let output = safe_cmd + .execute() + .map_err(|e| anyhow::anyhow!("Failed to run ClamAV scan: {}", e))?; let stdout = String::from_utf8_lossy(&output.stdout); let mut threats = Vec::new(); @@ -665,9 +682,9 @@ impl AntivirusManager { "freshclam" }; - let output = Command::new(freshclam) - .output() - .context("Failed to run freshclam")?; + let output = SafeCommand::new(freshclam) + .and_then(|cmd| cmd.execute()) + .map_err(|e| anyhow::anyhow!("Failed to run freshclam: {}", e))?; if output.status.success() { let mut status = self.protection_status.write().await; diff --git a/src/security/cert_pinning.rs b/src/security/cert_pinning.rs index 126eae84..0a3cb3a6 100644 --- a/src/security/cert_pinning.rs +++ b/src/security/cert_pinning.rs @@ -256,7 +256,10 @@ impl CertPinningManager { } pub fn is_enabled(&self) -> bool { - self.config.read().expect("config lock").enabled + self.config + .read() + .map(|c| c.enabled) + .unwrap_or(false) } pub fn add_pin(&self, pin: PinnedCert) -> Result<()> { diff --git a/src/security/command_guard.rs b/src/security/command_guard.rs index e5ac55cb..9353c588 100644 --- a/src/security/command_guard.rs +++ b/src/security/command_guard.rs @@ -17,6 +17,8 @@ static ALLOWED_COMMANDS: LazyLock> = LazyLock::new(|| { "convert", "gs", "tesseract", + "which", + "where", ]) });