import type { HookEvent, SecurityCheckResult } from '../../src/types.js'; import { writeAuditLog } from '../../src/utils/logger.js'; import { formatStartupReport } from '../../src/utils/alert.js'; /** * Deep-get helper with optional chaining safety. * cfg is typed as Record since the OpenClaw config * shape is not exported — we use safe runtime traversal. */ function get(obj: unknown, path: string): unknown { let current: unknown = obj; for (const key of path.split('.')) { if (current == null || typeof current !== 'object') return undefined; current = (current as Record)[key]; } return current; } function runChecks(cfg: Record): SecurityCheckResult[] { const checks: SecurityCheckResult[] = []; // 1. Auth mode — cfg.gateway.auth.mode const authMode = get(cfg, 'gateway.auth.mode') as string | undefined; if (!authMode || authMode === 'none') { checks.push({ name: 'Auth Mode', status: 'fail', message: authMode ? 'Auth mode is "none" — anyone can connect without authentication' : 'Auth mode is not configured — defaults may be insecure', }); } else { checks.push({ name: 'Auth Mode', status: 'pass', message: `Authentication mode: "${authMode}"`, }); } // 2. Auth token/password presence — cfg.gateway.auth.token / .password const hasToken = !!get(cfg, 'gateway.auth.token'); const hasPassword = !!get(cfg, 'gateway.auth.password'); if (authMode === 'token' && !hasToken) { checks.push({ name: 'Auth Credential', status: 'fail', message: 'Auth mode is "token" but no token is configured', }); } else if (authMode === 'password' && !hasPassword) { checks.push({ name: 'Auth Credential', status: 'fail', message: 'Auth mode is "password" but no password is configured', }); } else { checks.push({ name: 'Auth Credential', status: 'pass', message: 'Auth credentials are configured', }); } // 3. Bind profile — cfg.gateway.bind const bind = get(cfg, 'gateway.bind') as string | undefined; if (bind === 'lan' || bind === 'custom') { checks.push({ name: 'Network Binding', status: 'warn', message: `Bind profile is "${bind}" — gateway is exposed beyond localhost`, }); } else { checks.push({ name: 'Network Binding', status: 'pass', message: `Bind profile: "${bind ?? 'auto'}" (default or loopback)`, }); } // 4. Sandbox mode — cfg.agents.defaults.sandbox.mode const sandboxMode = get(cfg, 'agents.defaults.sandbox.mode') as string | undefined; if (!sandboxMode || sandboxMode === 'none' || sandboxMode === 'disabled') { checks.push({ name: 'Sandbox', status: 'warn', message: 'Sandbox mode is not enabled — consider enabling for untrusted agents', }); } else { checks.push({ name: 'Sandbox', status: 'pass', message: `Sandbox mode: "${sandboxMode}"`, }); } // 5. Rate limiting — cfg.gateway.auth.rateLimit.maxAttempts const maxAttempts = get(cfg, 'gateway.auth.rateLimit.maxAttempts') as number | undefined; if (!maxAttempts || maxAttempts <= 0) { checks.push({ name: 'Rate Limiting', status: 'warn', message: 'Auth rate limiting is not configured', }); } else { checks.push({ name: 'Rate Limiting', status: 'pass', message: `Rate limit: ${maxAttempts} max attempts`, }); } // 6. Hooks enabled — cfg.hooks.internal.enabled const hooksEnabled = get(cfg, 'hooks.internal.enabled'); if (hooksEnabled === false) { checks.push({ name: 'Internal Hooks', status: 'warn', message: 'Internal hooks are disabled — security hooks will not run', }); } else { checks.push({ name: 'Internal Hooks', status: 'pass', message: 'Internal hooks are enabled', }); } // 7. Exec approval — cfg.approvals.exec.enabled const execApproval = get(cfg, 'approvals.exec.enabled'); if (execApproval === false) { checks.push({ name: 'Exec Approval', status: 'warn', message: 'Exec approval is disabled — agents can run commands without confirmation', }); } else { checks.push({ name: 'Exec Approval', status: 'pass', message: 'Exec approval is enabled', }); } return checks; } const handler = async (event: HookEvent): Promise => { try { const { type, action, context, sessionKey, timestamp, messages } = event; if (type !== 'gateway' || action !== 'startup') return; const cfg = (context.cfg ?? {}) as Record; const checks = runChecks(cfg); const report = formatStartupReport(checks); messages.push(report); const passed = checks.filter((c) => c.status === 'pass').length; const score = Math.round((passed / checks.length) * 100); writeAuditLog({ timestamp: timestamp.toISOString(), event: `${type}:${action}`, sessionKey, alerts: [], metadata: { securityScore: score, checks: checks.map((c) => ({ name: c.name, status: c.status })), }, }); } catch (err) { console.error( '[security-guardrails:startup-checker] Error:', err instanceof Error ? err.message : String(err), ); } }; export default handler;