Files
Benjamin Yang c67775c1a0 feat: clean tokenization system implementation (#1874)
Core tokenization functionality with minimal file changes:

 Core Features:
- Intelligent tokenization engine (tokenizer.go)
- Context-aware secret classification (PASSWORD, APIKEY, DATABASE, etc.)
- Cross-file correlation with deterministic HMAC-SHA256 tokens
- Optional encrypted mapping for token→original value resolution

 Integration:
- CLI flags: --tokenize, --redaction-map, --encrypt-redaction-map
- Updated all redactor types: literal, single-line, multi-line, YAML
- Support bundle integration with auto-upload compatibility
- Backward compatibility: preserves ***HIDDEN*** when disabled

 Production Ready:
- Only 11 essential files (vs 31 in original PR)
- No excessive test files or documentation
- Clean build, all functionality verified
- Maintains existing redaction behavior by default

Token format: ***TOKEN_<TYPE>_<HASH>*** (e.g., ***TOKEN_PASSWORD_A1B2C3***)
2025-09-30 15:04:23 -05:00

147 lines
3.2 KiB
Go

package redact
import (
"bufio"
"bytes"
"io"
"path/filepath"
"strconv"
"strings"
"gopkg.in/yaml.v2"
"k8s.io/klog/v2"
)
type YamlRedactor struct {
maskPath []string
foundMatch bool
filePath string
redactName string
isDefault bool
}
func NewYamlRedactor(yamlPath, filePath, name string) *YamlRedactor {
pathComponents := strings.Split(yamlPath, ".")
return &YamlRedactor{maskPath: pathComponents, filePath: filePath, redactName: name}
}
func (r *YamlRedactor) Redact(input io.Reader, path string) io.Reader {
if r.filePath != "" {
match, err := filepath.Match(r.filePath, path)
if err != nil {
klog.Errorf("Failed to match %q and %q: %v", r.filePath, path, err)
return input
}
if !match {
return input
}
}
reader, writer := io.Pipe()
go func() {
var err error
defer func() {
if err == io.EOF {
writer.Close()
} else {
writer.CloseWithError(err)
}
}()
reader := bufio.NewReader(input)
var doc []byte
doc, err = io.ReadAll(reader)
var yamlInterface interface{}
err = yaml.Unmarshal(doc, &yamlInterface)
if err != nil {
buf := bytes.NewBuffer(doc)
buf.WriteTo(writer)
err = nil // this is not a fatal error
return
}
newYaml := r.redactYaml(yamlInterface, r.maskPath)
if !r.foundMatch {
// no match found, so make no changes
buf := bytes.NewBuffer(doc)
buf.WriteTo(writer)
return
}
var newBytes []byte
newBytes, err = yaml.Marshal(newYaml)
if err != nil {
return
}
buf := bytes.NewBuffer(newBytes)
buf.WriteTo(writer)
addRedaction(Redaction{
RedactorName: r.redactName,
CharactersRemoved: len(doc) - len(newBytes),
Line: 0, // line 0 because we have no way to tell what line was impacted
File: path,
IsDefaultRedactor: r.isDefault,
})
}()
return reader
}
func (r *YamlRedactor) redactYaml(in interface{}, path []string) interface{} {
if len(path) == 0 {
r.foundMatch = true
// Use tokenization if enabled
tokenizer := GetGlobalTokenizer()
if tokenizer.IsEnabled() {
// Convert the value to string and tokenize it
if valueStr, ok := in.(string); ok && valueStr != "" {
context := r.redactName
return tokenizer.TokenizeValueWithPath(valueStr, context, r.filePath)
}
}
return MASK_TEXT
}
switch typed := in.(type) {
case []interface{}:
// check if first path element is * - if it is, run redact on all children
if path[0] == "*" {
var newArr []interface{}
for _, child := range typed {
newChild := r.redactYaml(child, path[1:])
newArr = append(newArr, newChild)
}
return newArr
}
// check if first path element is an integer - if it is, run redact on that child
pathIdx, err := strconv.Atoi(path[0])
if err != nil {
return typed
}
if len(typed) > pathIdx {
child := typed[pathIdx]
typed[pathIdx] = r.redactYaml(child, path[1:])
return typed
}
return typed
case map[interface{}]interface{}:
if path[0] == "*" && len(typed) > 0 {
newMap := map[interface{}]interface{}{}
for key, child := range typed {
newMap[key] = r.redactYaml(child, path[1:])
}
return newMap
}
child, ok := typed[path[0]]
if ok {
newChild := r.redactYaml(child, path[1:])
typed[path[0]] = newChild
}
return typed
default:
return typed
}
}