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

127 lines
3.1 KiB
Go

package redact
import (
"bufio"
"bytes"
"fmt"
"io"
"regexp"
"github.com/replicatedhq/troubleshoot/pkg/constants"
"k8s.io/klog/v2"
)
type SingleLineRedactor struct {
scan *regexp.Regexp
re *regexp.Regexp
maskText string
filePath string
redactName string
isDefault bool
}
var NEW_LINE = []byte{'\n'}
func NewSingleLineRedactor(re LineRedactor, maskText, path, name string, isDefault bool) (*SingleLineRedactor, error) {
var scanCompiled *regexp.Regexp
compiled, err := compileRegex(re.regex)
if err != nil {
return nil, err
}
if re.scan != "" {
scanCompiled, err = compileRegex(re.scan)
if err != nil {
return nil, err
}
}
return &SingleLineRedactor{scan: scanCompiled, re: compiled, maskText: maskText, filePath: path, redactName: name, isDefault: isDefault}, nil
}
func (r *SingleLineRedactor) Redact(input io.Reader, path string) io.Reader {
out, writer := io.Pipe()
go func() {
var err error
defer func() {
if err == nil || err == io.EOF {
writer.Close()
} else {
if err == bufio.ErrTooLong {
s := fmt.Sprintf("Error redacting %q. A line in the file exceeded %d MB max length", path, constants.SCANNER_MAX_SIZE/1024/1024)
klog.V(2).Info(s)
} else {
klog.V(2).Info(fmt.Sprintf("Error redacting %q: %v", path, err))
}
writer.CloseWithError(err)
}
}()
buf := make([]byte, constants.BUF_INIT_SIZE)
scanner := bufio.NewScanner(input)
scanner.Buffer(buf, constants.SCANNER_MAX_SIZE)
tokenizer := GetGlobalTokenizer()
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Bytes()
// is scan is not nil, then check if line matches scan by lowercasing it
if r.scan != nil {
lowerLine := bytes.ToLower(line)
if !r.scan.Match(lowerLine) {
// Append newline since scanner strips it
err = writeBytes(writer, line, NEW_LINE)
if err != nil {
return
}
continue
}
}
// if scan matches, but re does not, do not redact
if !r.re.Match(line) {
// Append newline since scanner strips it
err = writeBytes(writer, line, NEW_LINE)
if err != nil {
return
}
continue
}
var clean []byte
if tokenizer.IsEnabled() {
// Use tokenized replacement - context comes from the redactor name which often indicates the secret type
context := r.redactName
clean = getTokenizedReplacementPatternWithPath(r.re, line, context, r.filePath)
} else {
// Use original masking behavior
substStr := []byte(getReplacementPattern(r.re, r.maskText))
clean = r.re.ReplaceAll(line, substStr)
}
// Append newline since scanner strips it
err = writeBytes(writer, clean, NEW_LINE)
if err != nil {
return
}
// if clean is not equal to line, a redaction was performed
if !bytes.Equal(clean, line) {
addRedaction(Redaction{
RedactorName: r.redactName,
CharactersRemoved: len(line) - len(clean),
Line: lineNum,
File: r.filePath,
IsDefaultRedactor: r.isDefault,
})
}
}
if scanErr := scanner.Err(); scanErr != nil {
err = scanErr
}
}()
return out
}