multica/server/pkg/redact/redact.go
Jiayuan 0491350f1b feat(security): add agent output redaction and private agent assignment enforcement
- Add redact package to detect and mask secrets (AWS keys, private keys,
  API tokens, bearer tokens, credentials, home paths) in agent output
  before posting as comments in TaskService
- Enforce agent visibility on issue assignment: private agents can only
  be assigned by their owner or workspace admins
- Add visibility picker (workspace/private) to CreateAgentDialog,
  default to private
- Grey out unassignable private agents in the assignee picker with
  lock icon indicator
2026-03-30 22:22:04 +08:00

71 lines
2.4 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, fine-grained, OAuth, etc.)
{regexp.MustCompile(`\b(ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9_]{36,255}\b`), "[REDACTED GITHUB TOKEN]"},
// OpenAI / Anthropic API keys
{regexp.MustCompile(`\bsk-[A-Za-z0-9_-]{20,}\b`), "[REDACTED API KEY]"},
// Slack tokens
{regexp.MustCompile(`\bxox[bporas]-[A-Za-z0-9\-]{10,}\b`), "[REDACTED SLACK TOKEN]"},
// Generic "Bearer <token>" in output
{regexp.MustCompile(`(?i)\bBearer\s+[A-Za-z0-9\-._~+/]+=*\b`), "Bearer [REDACTED]"},
// Generic key=value patterns for common secret env var names
{regexp.MustCompile(`(?i)(?:API_KEY|API_SECRET|SECRET_KEY|ACCESS_TOKEN|AUTH_TOKEN|PRIVATE_KEY|DATABASE_URL|DB_PASSWORD|REDIS_URL)\s*[=:]\s*\S+`), "[REDACTED CREDENTIAL]"},
}
// 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
}