Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
92 lines
3.6 KiB
Go
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)
|
|
}
|