mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-10 10:10:49 +00:00
75c570e951
- Secure CLI credential injection via AES-256-GCM encrypted env vars - API key management with fine-grained RBAC scopes - resolveAuth/requireAuth middleware across all 25+ HTTP handlers - In-memory API key cache with TTL, negative caching, pubsub invalidation - Sandbox-first execution (fails if unavailable, no silent fallback) - Credential scrubbing, constant-time token comparison, Admin-only CLI creds - SQL migration 000020: secure_cli_binaries + api_keys tables - 14 unit tests for cache and RBAC with race detector Closes #197
141 lines
4.4 KiB
Go
141 lines
4.4 KiB
Go
package tools
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
// Credential patterns to scrub from tool output before returning to the LLM.
|
|
// Inspired by zeroclaw's credential scrubbing system.
|
|
var credentialPatterns = []*regexp.Regexp{
|
|
// OpenAI
|
|
regexp.MustCompile(`sk-[a-zA-Z0-9]{20,}`),
|
|
// Anthropic
|
|
regexp.MustCompile(`sk-ant-[a-zA-Z0-9-]{20,}`),
|
|
// GitHub personal access tokens
|
|
regexp.MustCompile(`ghp_[a-zA-Z0-9]{36}`),
|
|
regexp.MustCompile(`gho_[a-zA-Z0-9]{36}`),
|
|
regexp.MustCompile(`ghu_[a-zA-Z0-9]{36}`),
|
|
regexp.MustCompile(`ghs_[a-zA-Z0-9]{36}`),
|
|
regexp.MustCompile(`ghr_[a-zA-Z0-9]{36}`),
|
|
// AWS
|
|
regexp.MustCompile(`AKIA[A-Z0-9]{16}`),
|
|
// Generic key=value patterns (case-insensitive)
|
|
regexp.MustCompile(`(?i)(api[_-]?key|token|secret|password|bearer|authorization)\s*[:=]\s*["']?\S{8,}["']?`),
|
|
|
|
// Connection strings (PostgreSQL, MySQL, MongoDB, Redis, AMQP)
|
|
regexp.MustCompile(`(?i)(postgres|postgresql|mysql|mongodb|redis|amqp)://[^\s"']+`),
|
|
// Generic KEY=/SECRET=/CREDENTIAL= env-var patterns (skip already-redacted [REDACTED] values)
|
|
regexp.MustCompile(`(?i)[A-Z_]*(KEY|SECRET|CREDENTIAL|PRIVATE)[A-Z_]*\s*=\s*[^\[\s]{8,}`),
|
|
// DSN/DATABASE_URL env vars (skip already-redacted values)
|
|
regexp.MustCompile(`(?i)(DSN|DATABASE_URL|REDIS_URL|MONGO_URI)\s*=\s*[^\[\s]{8,}`),
|
|
// VIRTUAL_* env vars (internal runtime config, should not leak)
|
|
regexp.MustCompile(`(?i)VIRTUAL_[A-Z_]+\s*=\s*[^\[\s]{4,}`),
|
|
// Long hex strings (64+ chars) — likely encryption keys, hashes, or secrets
|
|
regexp.MustCompile(`[a-fA-F0-9]{64,}`),
|
|
}
|
|
|
|
const redactedPlaceholder = "[REDACTED]"
|
|
const serverIPPlaceholder = "[SERVER_IP]"
|
|
|
|
// dynamicScrubValues holds runtime-discovered values to scrub (e.g., server IPs).
|
|
var (
|
|
dynamicScrubMu sync.RWMutex
|
|
dynamicScrubValues []string
|
|
)
|
|
|
|
// credentialScrubValues holds credential values to scrub (replaced with [REDACTED]).
|
|
// Separate from dynamicScrubValues which use [SERVER_IP] placeholder.
|
|
var (
|
|
credentialScrubMu sync.RWMutex
|
|
credentialScrubValues []string
|
|
)
|
|
|
|
// AddDynamicScrubValues adds exact string values to the dynamic scrub list.
|
|
// Thread-safe. Deduplicates. Empty strings are ignored.
|
|
func AddDynamicScrubValues(values ...string) {
|
|
dynamicScrubMu.Lock()
|
|
defer dynamicScrubMu.Unlock()
|
|
|
|
existing := make(map[string]bool, len(dynamicScrubValues))
|
|
for _, v := range dynamicScrubValues {
|
|
existing[v] = true
|
|
}
|
|
for _, v := range values {
|
|
if v != "" && !existing[v] {
|
|
dynamicScrubValues = append(dynamicScrubValues, v)
|
|
existing[v] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// DynamicScrubCount returns the number of dynamic scrub values registered.
|
|
func DynamicScrubCount() int {
|
|
dynamicScrubMu.RLock()
|
|
defer dynamicScrubMu.RUnlock()
|
|
return len(dynamicScrubValues)
|
|
}
|
|
|
|
// ResetDynamicScrubValues clears all dynamic scrub values. For testing only.
|
|
func ResetDynamicScrubValues() {
|
|
dynamicScrubMu.Lock()
|
|
defer dynamicScrubMu.Unlock()
|
|
dynamicScrubValues = nil
|
|
}
|
|
|
|
// ResetCredentialScrubValues clears all credential scrub values.
|
|
// Called when credentials are updated to prevent stale values from accumulating.
|
|
func ResetCredentialScrubValues() {
|
|
credentialScrubMu.Lock()
|
|
defer credentialScrubMu.Unlock()
|
|
credentialScrubValues = nil
|
|
}
|
|
|
|
// AddCredentialScrubValues adds credential values to the scrub list.
|
|
// These are replaced with [REDACTED] (not [SERVER_IP]).
|
|
// Thread-safe. Deduplicates. Empty/short strings ignored.
|
|
func AddCredentialScrubValues(values ...string) {
|
|
credentialScrubMu.Lock()
|
|
defer credentialScrubMu.Unlock()
|
|
|
|
existing := make(map[string]bool, len(credentialScrubValues))
|
|
for _, v := range credentialScrubValues {
|
|
existing[v] = true
|
|
}
|
|
for _, v := range values {
|
|
// Skip short values to avoid false-positive scrubbing
|
|
if len(v) >= 6 && !existing[v] {
|
|
credentialScrubValues = append(credentialScrubValues, v)
|
|
existing[v] = true
|
|
}
|
|
}
|
|
}
|
|
|
|
// ScrubCredentials replaces known credential patterns and dynamic values in text.
|
|
func ScrubCredentials(text string) string {
|
|
for _, pat := range credentialPatterns {
|
|
text = pat.ReplaceAllString(text, redactedPlaceholder)
|
|
}
|
|
|
|
// Credential values (from credentialed exec)
|
|
credentialScrubMu.RLock()
|
|
credVals := credentialScrubValues
|
|
credentialScrubMu.RUnlock()
|
|
|
|
for _, v := range credVals {
|
|
text = strings.ReplaceAll(text, v, redactedPlaceholder)
|
|
}
|
|
|
|
// Dynamic values (server IPs, etc.)
|
|
dynamicScrubMu.RLock()
|
|
vals := dynamicScrubValues
|
|
dynamicScrubMu.RUnlock()
|
|
|
|
for _, v := range vals {
|
|
text = strings.ReplaceAll(text, v, serverIPPlaceholder)
|
|
}
|
|
|
|
return text
|
|
}
|