| package executor |
|
|
| import ( |
| "regexp" |
| "sort" |
| "strings" |
| "unicode/utf8" |
|
|
| "github.com/tidwall/gjson" |
| "github.com/tidwall/sjson" |
| ) |
|
|
| |
| const zeroWidthSpace = "\u200B" |
|
|
| |
| type SensitiveWordMatcher struct { |
| regex *regexp.Regexp |
| } |
|
|
| |
| |
| func buildSensitiveWordMatcher(words []string) *SensitiveWordMatcher { |
| if len(words) == 0 { |
| return nil |
| } |
|
|
| |
| var validWords []string |
| for _, w := range words { |
| w = strings.TrimSpace(w) |
| if utf8.RuneCountInString(w) >= 2 && !strings.Contains(w, zeroWidthSpace) { |
| validWords = append(validWords, w) |
| } |
| } |
|
|
| if len(validWords) == 0 { |
| return nil |
| } |
|
|
| |
| sort.Slice(validWords, func(i, j int) bool { |
| return len(validWords[i]) > len(validWords[j]) |
| }) |
|
|
| |
| escaped := make([]string, len(validWords)) |
| for i, w := range validWords { |
| escaped[i] = regexp.QuoteMeta(w) |
| } |
|
|
| pattern := "(?i)" + strings.Join(escaped, "|") |
| re, err := regexp.Compile(pattern) |
| if err != nil { |
| return nil |
| } |
|
|
| return &SensitiveWordMatcher{regex: re} |
| } |
|
|
| |
| func obfuscateWord(word string) string { |
| if strings.Contains(word, zeroWidthSpace) { |
| return word |
| } |
|
|
| |
| r, size := utf8.DecodeRuneInString(word) |
| if r == utf8.RuneError || size >= len(word) { |
| return word |
| } |
|
|
| return string(r) + zeroWidthSpace + word[size:] |
| } |
|
|
| |
| func (m *SensitiveWordMatcher) obfuscateText(text string) string { |
| if m == nil || m.regex == nil { |
| return text |
| } |
| return m.regex.ReplaceAllStringFunc(text, obfuscateWord) |
| } |
|
|
| |
| |
| func obfuscateSensitiveWords(payload []byte, matcher *SensitiveWordMatcher) []byte { |
| if matcher == nil || matcher.regex == nil { |
| return payload |
| } |
|
|
| |
| payload = obfuscateSystemBlocks(payload, matcher) |
|
|
| |
| payload = obfuscateMessages(payload, matcher) |
|
|
| return payload |
| } |
|
|
| |
| func obfuscateSystemBlocks(payload []byte, matcher *SensitiveWordMatcher) []byte { |
| system := gjson.GetBytes(payload, "system") |
| if !system.Exists() { |
| return payload |
| } |
|
|
| if system.IsArray() { |
| modified := false |
| system.ForEach(func(key, value gjson.Result) bool { |
| if value.Get("type").String() == "text" { |
| text := value.Get("text").String() |
| obfuscated := matcher.obfuscateText(text) |
| if obfuscated != text { |
| path := "system." + key.String() + ".text" |
| payload, _ = sjson.SetBytes(payload, path, obfuscated) |
| modified = true |
| } |
| } |
| return true |
| }) |
| if modified { |
| return payload |
| } |
| } else if system.Type == gjson.String { |
| text := system.String() |
| obfuscated := matcher.obfuscateText(text) |
| if obfuscated != text { |
| payload, _ = sjson.SetBytes(payload, "system", obfuscated) |
| } |
| } |
|
|
| return payload |
| } |
|
|
| |
| func obfuscateMessages(payload []byte, matcher *SensitiveWordMatcher) []byte { |
| messages := gjson.GetBytes(payload, "messages") |
| if !messages.Exists() || !messages.IsArray() { |
| return payload |
| } |
|
|
| messages.ForEach(func(msgKey, msg gjson.Result) bool { |
| content := msg.Get("content") |
| if !content.Exists() { |
| return true |
| } |
|
|
| msgPath := "messages." + msgKey.String() |
|
|
| if content.Type == gjson.String { |
| |
| text := content.String() |
| obfuscated := matcher.obfuscateText(text) |
| if obfuscated != text { |
| payload, _ = sjson.SetBytes(payload, msgPath+".content", obfuscated) |
| } |
| } else if content.IsArray() { |
| |
| content.ForEach(func(blockKey, block gjson.Result) bool { |
| if block.Get("type").String() == "text" { |
| text := block.Get("text").String() |
| obfuscated := matcher.obfuscateText(text) |
| if obfuscated != text { |
| path := msgPath + ".content." + blockKey.String() + ".text" |
| payload, _ = sjson.SetBytes(payload, path, obfuscated) |
| } |
| } |
| return true |
| }) |
| } |
|
|
| return true |
| }) |
|
|
| return payload |
| } |
|
|