fix: use one content classifier across all log redaction paths
This commit is contained in:
@@ -16,18 +16,36 @@ import (
|
||||
|
||||
const redacted = "[REDACTED]"
|
||||
|
||||
func sensitive(key string) bool {
|
||||
normalized := strings.Map(func(r rune) rune {
|
||||
func normalizedField(key string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if unicode.IsLetter(r) || unicode.IsDigit(r) {
|
||||
return unicode.ToLower(r)
|
||||
}
|
||||
return -1
|
||||
}, key)
|
||||
}
|
||||
|
||||
// Content ownership is defined once for JSON, markup, scalar logs and text
|
||||
// continuation. It always implies sensitivity; credentials own only values.
|
||||
func conversationField(key string) bool {
|
||||
normalized := normalizedField(key)
|
||||
switch normalized {
|
||||
case "privatekey", "privatekeypem", "clientkey", "clientkeypem", "serverkey", "serverkeypem", "content", "headers", "history", "sentence", "sentences", "words", "openingremark", "openingremarks", "greeting", "sessionnonce", "credential", "credentials", "key", "authorization", "proxyauthorization", "apikey", "accesskeyid", "accesskeysecret", "appkey", "appsecret", "secret", "secretkey", "password", "passwd", "token", "accesstoken", "refreshtoken", "uploadtoken", "providertoken", "sessioncredential", "clientnonce", "servernonce", "cookie", "setcookie", "prompt", "systemprompt", "instructions", "messages", "messagecontent", "transcript", "transcription", "finaltranscript", "text", "utterance", "audio", "audiodata", "recordingdata", "raw", "closingremark", "signature", "targeturl":
|
||||
case "prompt", "systemprompt", "systemmessage", "instructions", "messages", "conversation", "history", "chathistory", "conversationhistory", "dialogue", "dialog", "transcript", "transcription", "finaltranscript", "messagecontent", "text", "content", "utterance", "sentence", "sentences", "words", "openingremark", "openingremarks", "greeting", "closingremark", "audio", "audiodata", "recordingdata", "raw":
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(normalized, "password") || strings.HasSuffix(normalized, "secret") || strings.HasSuffix(normalized, "apikey") || strings.HasSuffix(normalized, "token") || strings.HasSuffix(normalized, "text")
|
||||
return strings.HasSuffix(normalized, "text")
|
||||
}
|
||||
|
||||
func sensitive(key string) bool {
|
||||
if conversationField(key) {
|
||||
return true
|
||||
}
|
||||
normalized := normalizedField(key)
|
||||
switch normalized {
|
||||
case "privatekey", "privatekeypem", "clientkey", "clientkeypem", "serverkey", "serverkeypem", "headers", "sessionnonce", "credential", "credentials", "key", "authorization", "proxyauthorization", "apikey", "accesskeyid", "accesskeysecret", "appkey", "appsecret", "secret", "secretkey", "password", "passwd", "token", "accesstoken", "refreshtoken", "uploadtoken", "providertoken", "sessioncredential", "clientnonce", "servernonce", "cookie", "setcookie", "signature", "targeturl":
|
||||
return true
|
||||
}
|
||||
return strings.HasSuffix(normalized, "password") || strings.HasSuffix(normalized, "secret") || strings.HasSuffix(normalized, "apikey") || strings.HasSuffix(normalized, "token")
|
||||
}
|
||||
|
||||
type replacement struct {
|
||||
|
||||
@@ -116,16 +116,6 @@ func redactForm(body string) (string, bool) {
|
||||
|
||||
var textFieldLine = regexp.MustCompile(`^\s*([A-Za-z_][A-Za-z0-9_.-]*)\s*[:=]`)
|
||||
|
||||
func conversationField(key string) bool {
|
||||
key = strings.ToLower(strings.NewReplacer("_", "", "-", "", ".", "").Replace(key))
|
||||
switch key {
|
||||
case "prompt", "systemprompt", "systemmessage", "instructions", "messages", "transcript", "transcription", "text", "content", "audio", "closingremark", "conversation":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func redactConversationLines(body string) string {
|
||||
lines := strings.SplitAfter(body, "\n")
|
||||
hiding := false
|
||||
|
||||
@@ -29,6 +29,38 @@ func TestAssociatedSensitiveControlsHideContentWithoutSwallowingBusiness(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestContentFieldClassificationAndEveryLoggingPathAgree(t *testing.T) {
|
||||
fields := []string{"conversation", "history", "messages", "system_message", "final_transcript", "message_content", "utterance", "sentence", "sentences", "words", "opening_remark", "opening_remarks", "greeting", "chat_history", "conversation_history", "dialogue"}
|
||||
for _, field := range fields {
|
||||
t.Run(field, func(t *testing.T) {
|
||||
if !sensitive(field) || !conversationField(field) {
|
||||
t.Fatalf("content field classifications disagree: %s sensitive=%v continuation=%v", field, sensitive(field), conversationField(field))
|
||||
}
|
||||
textValue := "upstream failed\n" + field + ":\n\nuser: private-user-turn\n\nassistant: private-assistant-turn\n\nstatus=retry_pending\nattempts=2"
|
||||
jsonValue := `{"` + field + `":[{"role":"user","message":"private-user-turn"},{"role":"assistant","message":"private-assistant-turn"}],"task_id":4,"status":"retry_pending"}`
|
||||
for _, debug := range []bool{false, true} {
|
||||
out := installTestLogger(t, debug)
|
||||
slog.Error("failed", "error", errors.New(textValue), "detail", textValue)
|
||||
slog.Info(textValue, "stage", "failure_observed")
|
||||
slog.Error("failed", "error", errors.New("upstream: "+jsonValue), "detail", jsonValue)
|
||||
TextBody(t.Context(), "HTTP text observed", []byte(textValue), "transport", "http")
|
||||
Body(t.Context(), "HTTP JSON observed", []byte(jsonValue), "transport", "http")
|
||||
text := out.String()
|
||||
for _, secret := range []string{"private-user-turn", "private-assistant-turn"} {
|
||||
if strings.Contains(text, secret) {
|
||||
t.Fatalf("%s debug %v leak: %s", field, debug, text)
|
||||
}
|
||||
}
|
||||
for _, keep := range []string{"upstream failed", "retry_pending", "task_id", "attempts=2"} {
|
||||
if !strings.Contains(text, keep) {
|
||||
t.Fatal("business diagnostics lost", text)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationOwnershipPersistsAcrossBlankLines(t *testing.T) {
|
||||
for _, gap := range []string{"\n", "\n\n", "\n \n\t\n", "\r\n\r\n"} {
|
||||
for _, label := range []string{"messages", "prompt", "transcript", "content"} {
|
||||
|
||||
Reference in New Issue
Block a user