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("<<>>\n") b.WriteString(content) b.WriteString("\n<<>>") 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) }