mirror of
https://github.com/alibaba/higress.git
synced 2026-06-04 18:17:33 +08:00
feat(ai-security-guard): structured x_higress deny response, error-path metrics, and AI logging (#3894)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com> Co-authored-by: rinfx <yucheng.lxr@alibaba-inc.com>
This commit is contained in:
@@ -58,7 +58,7 @@ func ReplaceJsonFieldTextContent(body []byte, jsonPath string, newContent string
|
||||
fieldValue := gjson.GetBytes(body, resolved)
|
||||
if !fieldValue.IsArray() {
|
||||
// Simple string content — replace directly
|
||||
return sjson.SetBytes(body, resolved, newContent)
|
||||
return setJsonTextContent(body, resolved, newContent)
|
||||
}
|
||||
// Array content (multimodal): replace text items, preserve others
|
||||
result := body
|
||||
@@ -86,7 +86,7 @@ func ReplaceJsonFieldTextContent(body []byte, jsonPath string, newContent string
|
||||
// If there's only one text item, put all desensitized content there
|
||||
if len(textEntries) == 1 {
|
||||
itemPath := fmt.Sprintf("%s.%d.text", resolved, textEntries[0].index)
|
||||
return sjson.SetBytes(result, itemPath, newContent)
|
||||
return setJsonTextContent(result, itemPath, newContent)
|
||||
}
|
||||
// Multiple text items: split desensitized content proportionally by original lengths
|
||||
for j, entry := range textEntries {
|
||||
@@ -117,7 +117,7 @@ func ReplaceJsonFieldTextContent(body []byte, jsonPath string, newContent string
|
||||
remaining = remaining[byteOffset:]
|
||||
}
|
||||
itemPath := fmt.Sprintf("%s.%d.text", resolved, entry.index)
|
||||
result, err = sjson.SetBytes(result, itemPath, replacement)
|
||||
result, err = setJsonTextContent(result, itemPath, replacement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -125,6 +125,18 @@ func ReplaceJsonFieldTextContent(body []byte, jsonPath string, newContent string
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func setJsonTextContent(body []byte, jsonPath string, newContent string) ([]byte, error) {
|
||||
current := gjson.GetBytes(body, jsonPath)
|
||||
result, err := sjson.SetBytes(body, jsonPath, newContent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if current.Exists() && current.String() != newContent && bytes.Equal(result, body) {
|
||||
return nil, fmt.Errorf("failed to replace json path %q", jsonPath)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// resolveJsonPath converts gjson modifier paths (e.g. "messages.@reverse.0.content")
|
||||
// into concrete index paths (e.g. "messages.2.content") that sjson can handle.
|
||||
func resolveJsonPath(body []byte, jsonPath string) string {
|
||||
|
||||
@@ -263,6 +263,14 @@ func TestResolveJsonPathEdgeCases(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestReplaceJsonFieldTextContentReportsReadableNoOp(t *testing.T) {
|
||||
body := []byte(`{"messages":[{"role":"user","content":"敏感内容"}]}`)
|
||||
result, err := ReplaceJsonFieldTextContent(body, "@this.messages.0.content", "masked")
|
||||
if err == nil {
|
||||
t.Fatalf("expected error for readable path that sjson leaves unchanged, got nil with %s", string(result))
|
||||
}
|
||||
}
|
||||
|
||||
// TestReplaceJsonFieldContent covers the simple ReplaceJsonFieldContent function
|
||||
func TestReplaceJsonFieldContent(t *testing.T) {
|
||||
body := []byte(`{"messages":[{"role":"user","content":"original"}]}`)
|
||||
|
||||
Reference in New Issue
Block a user