mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-27 21:33:41 +02:00
The primary server redactor now covers github_pat_ and standard AIza keys, but the client transcript safety net does not. Add matching client patterns and regression coverage so previously stored or otherwise unredacted transcript values are masked at display time. Also make the fixed-length Google key rule redact a key ending in '-' while preserving its trailing delimiter. Keep the 39-character format exact to avoid broadening false positives. Co-authored-by: multica-agent <github@multica.ai>
121 lines
4.6 KiB
Go
121 lines
4.6 KiB
Go
// Package redact provides functions for detecting and masking secrets
|
|
// in agent output before it reaches the database or WebSocket broadcast.
|
|
package redact
|
|
|
|
import (
|
|
"os"
|
|
"os/user"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// secretPattern pairs a compiled regex with its replacement text.
|
|
type secretPattern struct {
|
|
re *regexp.Regexp
|
|
replacement string
|
|
}
|
|
|
|
// Patterns are checked in order; first match wins per position.
|
|
var patterns = []secretPattern{
|
|
// AWS access key IDs (always start with AKIA)
|
|
{regexp.MustCompile(`\bAKIA[0-9A-Z]{16}\b`), "[REDACTED AWS KEY]"},
|
|
|
|
// AWS secret access keys (40 char base64-ish, preceded by a common separator)
|
|
{regexp.MustCompile(`(?i)(?:aws_secret_access_key|secret_?access_?key)\s*[=:]\s*[A-Za-z0-9/+=]{40}`), "[REDACTED AWS SECRET]"},
|
|
|
|
// PEM private keys (multi-line)
|
|
{regexp.MustCompile(`(?s)-----BEGIN[A-Z\s]*PRIVATE KEY-----.*?-----END[A-Z\s]*PRIVATE KEY-----`), "[REDACTED PRIVATE KEY]"},
|
|
|
|
// GitHub tokens (classic PAT, OAuth, user-to-server, server-to-server, refresh)
|
|
{regexp.MustCompile(`\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}\b`), "[REDACTED GITHUB TOKEN]"},
|
|
|
|
// GitHub fine-grained personal access tokens use the github_pat_ prefix,
|
|
// which the classic ghp_/gho_/... pattern above does not cover. Without
|
|
// this line a fine-grained PAT emitted in agent output leaks unredacted
|
|
// to the database and WebSocket broadcast.
|
|
{regexp.MustCompile(`\bgithub_pat_[A-Za-z0-9_]{20,255}\b`), "[REDACTED GITHUB TOKEN]"},
|
|
|
|
// OpenAI / Anthropic API keys
|
|
{regexp.MustCompile(`\bsk-[A-Za-z0-9_-]{20,}\b`), "[REDACTED API KEY]"},
|
|
|
|
// Slack bot/user/legacy tokens. The char class includes 'e' so the
|
|
// newer xoxe- config/refresh tokens are covered alongside xoxb/p/o/r/a/s.
|
|
{regexp.MustCompile(`\bxox[bporase]-[A-Za-z0-9\-]{10,}\b`), "[REDACTED SLACK TOKEN]"},
|
|
|
|
// Slack app-level tokens use the xapp- prefix, which the xox*- rule above
|
|
// does not match. Without this an app-level token echoed in agent output
|
|
// leaks unredacted to the DB / WebSocket broadcast.
|
|
{regexp.MustCompile(`\bxapp-[A-Za-z0-9-]{10,}\b`), "[REDACTED SLACK TOKEN]"},
|
|
|
|
// GitLab personal access tokens
|
|
{regexp.MustCompile(`\bglpat-[A-Za-z0-9_-]{20,}\b`), "[REDACTED GITLAB TOKEN]"},
|
|
|
|
// Google API keys always start with the AIza prefix and are 39 chars total
|
|
// (AIza + 35). Capture and restore the trailing delimiter so keys ending in
|
|
// a non-word character such as '-' are still redacted.
|
|
{regexp.MustCompile(`\bAIza[0-9A-Za-z_-]{35}([^0-9A-Za-z_-]|$)`), "[REDACTED GOOGLE API KEY]$1"},
|
|
|
|
// Stripe secret / restricted live keys (sk_live_ / rk_live_). The sk-
|
|
// rule above only matches the hyphen form used by OpenAI/Anthropic; Stripe
|
|
// uses an underscore, so live keys are not covered without this. Publishable
|
|
// keys (pk_live_) are intentionally excluded — they are not secret.
|
|
{regexp.MustCompile(`\b(?:sk|rk)_live_[0-9A-Za-z]{16,}\b`), "[REDACTED STRIPE KEY]"},
|
|
|
|
// JWT tokens (three base64url segments)
|
|
{regexp.MustCompile(`\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b`), "[REDACTED JWT]"},
|
|
|
|
// Generic "Bearer <token>" in output
|
|
{regexp.MustCompile(`(?i)\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b`), "Bearer [REDACTED]"},
|
|
|
|
// Connection strings with embedded passwords
|
|
{regexp.MustCompile(`(?i)(?:postgres|mysql|mongodb|redis|amqp)(?:ql)?://[^:\s]+:[^@\s]+@`), "[REDACTED CONNECTION STRING]@"},
|
|
|
|
// Generic key=value patterns for common secret env var names
|
|
{regexp.MustCompile(`(?i)(?:API_KEY|API_SECRET|SECRET_KEY|SECRET|ACCESS_TOKEN|AUTH_TOKEN|PRIVATE_KEY|DATABASE_URL|DB_PASSWORD|DB_URL|REDIS_URL|PASSWORD|TOKEN)\s*[=:]\s*\S+`), "[REDACTED CREDENTIAL]"},
|
|
}
|
|
|
|
// InputMap returns a copy of m with all string values passed through Text.
|
|
// Non-string values are preserved as-is.
|
|
func InputMap(m map[string]any) map[string]any {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
out := make(map[string]any, len(m))
|
|
for k, v := range m {
|
|
if s, ok := v.(string); ok {
|
|
out[k] = Text(s)
|
|
} else {
|
|
out[k] = v
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// homeDir is resolved once at init for path redaction.
|
|
var homeDir string
|
|
var username string
|
|
|
|
func init() {
|
|
homeDir, _ = os.UserHomeDir()
|
|
if u, err := user.Current(); err == nil {
|
|
username = u.Username
|
|
}
|
|
}
|
|
|
|
// Text scans the input string for known secret patterns and replaces
|
|
// matches with safe placeholders. It also masks the local user's home
|
|
// directory path to prevent leaking the username.
|
|
func Text(s string) string {
|
|
for _, p := range patterns {
|
|
s = p.re.ReplaceAllString(s, p.replacement)
|
|
}
|
|
|
|
// Redact home directory paths (e.g. /Users/john/ → /Users/****/).
|
|
if homeDir != "" && username != "" {
|
|
masked := strings.Replace(homeDir, username, "****", 1)
|
|
s = strings.ReplaceAll(s, homeDir, masked)
|
|
}
|
|
|
|
return s
|
|
}
|