Files
kandev/apps/backend/internal/plugins/diagnostics.go
T
2026-08-02 22:25:27 +01:00

74 lines
2.9 KiB
Go

package plugins
import (
"os"
"regexp"
"strings"
"sync"
"unicode/utf8"
)
// maxPluginErrorBytes bounds the diagnostic persisted into a plugin record.
// Failure messages can originate in subprocess/transport errors, so keeping
// them bounded prevents an unexpectedly verbose error from bloating the
// record or the settings API response.
const maxPluginErrorBytes = 2048
var (
pluginBearerTokenPattern = regexp.MustCompile(`(?i)\bBearer\s+[A-Za-z0-9._~+/=-]+`)
pluginLabeledSecretPattern = regexp.MustCompile(`(?i)\b((?:[A-Za-z0-9]+[_-])*(?:password|passwd|passphrase|secret|token|api[_ -]?(?:key|token|secret)|access[_ -]?token|refresh[_ -]?token|pat))\b(\s*[:=]\s*)(?:"[^"]*"|'[^']*'|[^\s,;}]+)`)
pluginPATPattern = regexp.MustCompile(`\b(?:github_pat_[A-Za-z0-9_]+|gh[pousr]_[A-Za-z0-9]+|glpat-[A-Za-z0-9_-]+)\b`)
pluginHomePathPattern = regexp.MustCompile(`(/Users/[^/\s]+|/home/[^/\s]+|/root)(/|$)`)
pluginWindowsHomePattern = regexp.MustCompile(`(?i)([A-Za-z]:[/\\]Users[/\\][^/\\\s]+)([/\\]|$)`)
pluginURLCredentialPattern = regexp.MustCompile(`(?i)(https?|ftp)://[^:@/\s]+:[^@/\s]+@`)
pluginHomePatternOnce sync.Once
cachedPluginHomePattern *regexp.Regexp
)
// normalizePluginError turns a runtime failure into a compact, single-line
// diagnostic suitable for persistence and display. Runtime errors can include
// arbitrary go-plugin subprocess output, so credential-like values and home
// paths are removed before the bounded message is stored.
func normalizePluginError(err error) string {
if err == nil {
return ""
}
message := strings.Join(strings.Fields(err.Error()), " ")
message = strings.ToValidUTF8(message, "�")
message = redactPluginError(message)
if len([]byte(message)) <= maxPluginErrorBytes {
return message
}
return truncatePluginError(message)
}
func truncatePluginError(message string) string {
const marker = "…"
content := []byte(message)
cut := maxPluginErrorBytes - len([]byte(marker))
for cut > 0 && cut < len(content) && !utf8.RuneStart(content[cut]) {
cut--
}
return string(content[:cut]) + marker
}
func redactPluginError(message string) string {
pluginHomePatternOnce.Do(func() {
if home, err := os.UserHomeDir(); err == nil && home != "" && home != "/" {
cachedPluginHomePattern = regexp.MustCompile(regexp.QuoteMeta(home) + `([/\\]|$)`)
}
})
if cachedPluginHomePattern != nil {
message = cachedPluginHomePattern.ReplaceAllString(message, "~$1")
}
message = pluginHomePathPattern.ReplaceAllString(message, "~$2")
message = pluginWindowsHomePattern.ReplaceAllString(message, "~$2")
message = pluginURLCredentialPattern.ReplaceAllString(message, "${1}://[REDACTED]@")
message = pluginBearerTokenPattern.ReplaceAllString(message, "Bearer [REDACTED]")
message = pluginLabeledSecretPattern.ReplaceAllString(message, "${1}${2}[REDACTED]")
message = pluginPATPattern.ReplaceAllString(message, "[REDACTED]")
return message
}