internal/security/ — core security layer baked into gnoma: - Secret scanner: gitleaks-derived regex patterns (Anthropic, OpenAI, AWS, GitHub, GitLab, Slack, Stripe, private keys, DB URLs, generic secrets) + Shannon entropy detection for unknown formats - Redactor: replaces matched secrets with [REDACTED], merges overlapping ranges, preserves surrounding context - Unicode sanitizer: NFKC normalization, strips Cf/Co categories, tag characters (ASCII smuggling), zero-width chars, RTL overrides - Incognito mode: suppresses persistence, learning, content logging - Firewall: wraps engine, scans outgoing messages + system prompt + tool results before they reach the provider Wired into engine and CLI. 21 security tests.
52 lines
1.3 KiB
Go
52 lines
1.3 KiB
Go
package security
|
|
|
|
import "sort"
|
|
|
|
const redactedPlaceholder = "[REDACTED]"
|
|
|
|
// Redact replaces detected secrets in content with [REDACTED].
|
|
// Preserves surrounding context (quotes, delimiters).
|
|
func Redact(content string, matches []SecretMatch) string {
|
|
if len(matches) == 0 {
|
|
return content
|
|
}
|
|
|
|
// Filter to redact-only and sort by start position ascending
|
|
var redacts []SecretMatch
|
|
for _, m := range matches {
|
|
if m.Action == ActionRedact && m.Start >= 0 && m.End <= len(content) && m.Start < m.End {
|
|
redacts = append(redacts, m)
|
|
}
|
|
}
|
|
if len(redacts) == 0 {
|
|
return content
|
|
}
|
|
|
|
sort.Slice(redacts, func(i, j int) bool {
|
|
return redacts[i].Start < redacts[j].Start
|
|
})
|
|
|
|
// Merge overlapping ranges
|
|
merged := []SecretMatch{redacts[0]}
|
|
for _, m := range redacts[1:] {
|
|
last := &merged[len(merged)-1]
|
|
if m.Start <= last.End {
|
|
// Overlapping — extend the range
|
|
if m.End > last.End {
|
|
last.End = m.End
|
|
}
|
|
} else {
|
|
merged = append(merged, m)
|
|
}
|
|
}
|
|
|
|
// Build result replacing merged ranges from end to start
|
|
result := []byte(content)
|
|
for i := len(merged) - 1; i >= 0; i-- {
|
|
m := merged[i]
|
|
replacement := []byte(redactedPlaceholder)
|
|
result = append(result[:m.Start], append(replacement, result[m.End:]...)...)
|
|
}
|
|
return string(result)
|
|
}
|