Files
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

92 lines
3.6 KiB
Go

package security
import (
"regexp"
"strings"
)
const (
// MaxTicketPromptRunes caps subject+body text embedded in LLM prompts.
MaxTicketPromptRunes = 6000
// MaxKBSnippetRunes caps each KB snippet passed to the model.
MaxKBSnippetRunes = 2000
// MaxKBSnippets caps how many snippets may accompany one ticket prompt.
MaxKBSnippets = 5
)
// Secret patterns beyond campaign prompt soft-filters (ticket bodies are untrusted).
var (
reTicketBearer = regexp.MustCompile(`(?i)\b(Bearer|Basic)\s+[A-Za-z0-9\-._~+/]+=*`)
reTicketAssign = regexp.MustCompile(`(?i)\b((?:api[_-]?key|access[_-]?token|secret(?:_key)?|password|passwd|authorization|credential|private[_-]?key)\s*[=:]\s*)["']?[^\s"',}\]]+["']?`)
reTicketStripe = regexp.MustCompile(`\b(sk_live_|sk_test_|rk_live_|rk_test_|whsec_|pk_live_|pk_test_)[A-Za-z0-9]+`)
reTicketOpenAI = regexp.MustCompile(`\bsk-[A-Za-z0-9]{20,}`)
reTicketAWS = regexp.MustCompile(`\b(AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16})\b`)
reTicketPEM = regexp.MustCompile(`(?s)-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----.*?-----END [A-Z0-9 ]*PRIVATE KEY-----`)
reTicketDSN = regexp.MustCompile(`(?i)\b((?:mysql|postgres|postgresql|redis|rediss|mongodb):\/\/)[^@\s]+@`)
reTicketJWT = regexp.MustCompile(`\beyJ[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}`)
reTicketInject = regexp.MustCompile(`(?i)(ignore\s+(all\s+)?(previous|prior|above)\s+(instructions?|prompts?|rules?)|disregard\s+(all\s+)?(previous|prior)|system\s*:|assistant\s*:|<\s*/?\s*script|\[\s*INST\s*\]|<<\s*SYS\s*>>)`)
)
// RedactSecrets strips common secret material from untrusted ticket text before
// match features, prompts, or structured logs. Fail-soft: never panics.
func RedactSecrets(s string) (out string) {
defer func() {
if recover() != nil {
out = "[REDACTED]"
}
}()
out = s
out = reTicketPEM.ReplaceAllString(out, "[REDACTED_PEM]")
out = reTicketBearer.ReplaceAllString(out, "[REDACTED_AUTH]")
out = reTicketAssign.ReplaceAllString(out, "${1}[REDACTED]")
out = reTicketStripe.ReplaceAllString(out, "[REDACTED]")
out = reTicketOpenAI.ReplaceAllString(out, "[REDACTED]")
out = reTicketAWS.ReplaceAllString(out, "[REDACTED]")
out = reTicketJWT.ReplaceAllString(out, "[REDACTED]")
out = reTicketDSN.ReplaceAllString(out, "${1}[REDACTED]@")
return out
}
// SanitizeUntrustedTicketText redacts secrets, soft-filters injection phrases,
// strips controls, and truncates. Use for any ticket subject/body entering a prompt.
func SanitizeUntrustedTicketText(s string, maxRunes int) string {
if maxRunes <= 0 {
maxRunes = MaxTicketPromptRunes
}
s = RedactSecrets(s)
s = stripControls(strings.TrimSpace(s))
s = reTicketInject.ReplaceAllString(s, "[filtered]")
s = injectPhrase.ReplaceAllString(s, "[filtered]")
return TruncateRunes(s, maxRunes)
}
// WrapUntrustedData delimits untrusted customer content so models treat it as data,
// not instructions. Content must already be sanitized.
func WrapUntrustedData(label, content string) string {
label = strings.TrimSpace(label)
if label == "" {
label = "untrusted"
}
label = strings.Map(func(r rune) rune {
if r == '<' || r == '>' {
return '-'
}
return r
}, label)
var b strings.Builder
b.WriteString("<<<UNTRUSTED_")
b.WriteString(strings.ToUpper(label))
b.WriteString("_START>>>\n")
b.WriteString(content)
b.WriteString("\n<<<UNTRUSTED_")
b.WriteString(strings.ToUpper(label))
b.WriteString("_END>>>")
return b.String()
}
// SanitizeKBSnippet bounds and lightly sanitizes platform KB text for prompts.
// KB is admin-authored (trusted relative to tickets) but still length-capped.
func SanitizeKBSnippet(s string) string {
return SanitizePrompt(s, MaxKBSnippetRunes)
}