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:
JianweiWang
2026-05-29 10:45:10 +08:00
committed by GitHub
parent 385f8d8b4e
commit c21a38e783
14 changed files with 2181 additions and 195 deletions

View File

@@ -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 {

View File

@@ -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"}]}`)