Files
trufflehog/pkg/log/log_cores_test.go
Cody Rose 7635b24fd5 Allow logging of caller info (#4731)
This commit extends our logging wrapper code to allow the addition of automatically generated caller information to log entries. It also adds a way to selectively suppress this caller information for individual sinks. This commit does not change any of trufflehog's behavior - it just enables future functionality in case we want it to use it later.

This commit also provides an exportable type alias for sink configuration option functions so that callers have more flexibility in how they prepare groups of them.

This commit also adds some new tests for the existing redactionCore and fixes an existing bug in it. (The bug hasn't yet caused problems, but it created the potential for future problems.)
2026-02-05 17:01:09 -05:00

288 lines
8.3 KiB
Go

package log
import (
"bytes"
"encoding/json"
"io"
"strings"
"testing"
"github.com/go-logr/zapr"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// ignoreEverythingCore is a zap core that rejects all log entries. It is used to test that our custom cores respect
// wrapped core check logic.
type ignoreEverythingCore struct {
zapcore.Core
}
func (t *ignoreEverythingCore) With(fields []zapcore.Field) zapcore.Core {
return &ignoreEverythingCore{t.Core.With(fields)}
}
func (t *ignoreEverythingCore) Check(entry zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
return ce
}
var _ zapcore.Core = (*ignoreEverythingCore)(nil)
type TestSuite struct {
suite.Suite
oldRedactor *dynamicRedactor
}
func (ts *TestSuite) SetupTest() {
ts.oldRedactor = globalRedactor
globalRedactor = &dynamicRedactor{
denySet: make(map[string]struct{}),
}
globalRedactor.replacer.Store(strings.NewReplacer())
}
func (ts *TestSuite) TearDownTest() {
globalRedactor = ts.oldRedactor
}
func TestCustomCores(t *testing.T) {
suite.Run(t, new(TestSuite))
}
// This test confirms that when our custom redaction core wraps our custom caller suppression core, both redaction and
// caller suppression occur.
func (ts *TestSuite) TestCoreComposition_RedactionWrappingCallerSuppression() {
// Arrange: Create a testable log sink
var buf bytes.Buffer
baseCore := zapcore.NewCore(
zapcore.NewJSONEncoder(defaultEncoderConfig()),
zapcore.Lock(zapcore.AddSync(&buf)),
globalLogLevel)
// Arrange: Create a core stack and set up redaction
core := NewRedactionCore(&suppressCallerCore{baseCore}, globalRedactor)
RedactGlobally("sensitive")
// Arrange: Create a logger
logger := zapr.NewLogger(zap.New(core, zap.AddCaller()))
// Act
logger.Info("sensitive message")
// Assert that redaction executed correctly
msg := buf.String()
ts.Assert().Contains(msg, "message")
ts.Assert().NotContains(msg, "sensitive")
ts.Assert().NotContains(msg, "caller")
}
// This test confirms that when our custom caller suppression core wraps our custom redaction core, both redaction and
// caller suppression occur.
func (ts *TestSuite) TestCoreComposition_CallerSuppressionWrappingRedaction() {
// Arrange: Create a testable log sink
var buf bytes.Buffer
baseCore := zapcore.NewCore(
zapcore.NewJSONEncoder(defaultEncoderConfig()),
zapcore.Lock(zapcore.AddSync(&buf)),
globalLogLevel)
// Arrange: Create a core stack and set up redaction
core := &suppressCallerCore{NewRedactionCore(baseCore, globalRedactor)}
RedactGlobally("sensitive")
// Arrange: Create a logger
logger := zapr.NewLogger(zap.New(core, zap.AddCaller()))
// Act
logger.Info("sensitive message")
// Assert that redaction executed correctly
msg := buf.String()
ts.Assert().Contains(msg, "message")
ts.Assert().NotContains(msg, "sensitive")
ts.Assert().NotContains(msg, "caller")
}
// This test confirms that the redaction logic executes correctly for console sinks.
func (ts *TestSuite) TestGlobalRedaction_Console() {
var buf bytes.Buffer
logger, flush := New("console-redaction-test",
WithConsoleSink(&buf, WithGlobalRedaction()),
)
RedactGlobally("foo")
RedactGlobally("bar")
logger.Info("this foo is :bar",
"foo", "bar",
"array", []string{"foo", "bar", "baz"},
"object", map[string]string{"foo": "bar"})
ts.Require().NoError(flush())
gotParts := strings.Split(buf.String(), "\t")[1:] // The first item is the timestamp
wantParts := []string{
"info-0",
"console-redaction-test",
"this ***** is :*****",
"{\"foo\": \"*****\", \"array\": [\"foo\", \"bar\", \"baz\"], \"object\": {\"foo\":\"bar\"}}\n",
}
ts.Assert().Equal(wantParts, gotParts)
}
// This test confirms that the redaction logic executes correctly for JSON sinks.
func (ts *TestSuite) TestGlobalRedaction_JSON() {
var jsonBuffer bytes.Buffer
logger, flush := New("json-redaction-test",
WithJSONSink(&jsonBuffer, WithGlobalRedaction()),
)
RedactGlobally("foo")
RedactGlobally("bar")
logger.Info("this foo is :bar",
"foo", "bar",
"array", []string{"foo", "bar", "baz"},
"object", map[string]string{"foo": "bar"})
ts.Require().NoError(flush())
var parsedJSON map[string]any
ts.Require().NoError(json.Unmarshal(jsonBuffer.Bytes(), &parsedJSON))
ts.Assert().NotEmpty(parsedJSON["ts"])
delete(parsedJSON, "ts")
ts.Assert().Equal(
map[string]any{
"level": "info-0",
"logger": "json-redaction-test",
"msg": "this ***** is :*****",
"foo": "*****",
"array": []any{"foo", "bar", "baz"},
"object": map[string]interface{}{"foo": "bar"},
},
parsedJSON,
)
}
// This test confirms that our custom redaction core respects the "check" logic of any cores it wraps.
func (ts *TestSuite) TestRedactionCore_RespectsWrappedCheckLogic() {
// Arrange: Create a testable log sink
var buf bytes.Buffer
baseCore := zapcore.NewCore(
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
zapcore.Lock(zapcore.AddSync(&buf)),
globalLogLevel)
// Arrange: Set up a core stack
core := NewRedactionCore(&ignoreEverythingCore{baseCore}, globalRedactor)
// Arrange: Create a logger
logger := zapr.NewLogger(zap.New(core))
// Act
logger.Info("message")
// Assert that the wrapped core's check logic was respected
msg := buf.String()
ts.Assert().Empty(msg)
}
// This test confirms that our custom caller suppression core respects the "check" logic of any cores it wraps.
func (ts *TestSuite) TestSuppressCallerCore_RespectsWrappedCheckLogic() {
// Arrange: Create a testable log sink
var buf bytes.Buffer
baseCore := zapcore.NewCore(
zapcore.NewConsoleEncoder(defaultEncoderConfig()),
zapcore.Lock(zapcore.AddSync(&buf)),
globalLogLevel)
// Arrange: Set up a core stack
core := &suppressCallerCore{&ignoreEverythingCore{baseCore}}
// Arrange: Create a logger
logger := zapr.NewLogger(zap.New(core))
// Act
logger.Info("message")
// Assert that the wrapped core's check logic was respected
msg := buf.String()
ts.Assert().Empty(msg)
}
// This test confirms that our custom caller suppression core suppresses caller information.
func (ts *TestSuite) TestSuppressCallerCore_SuppressesCaller() {
// Arrange: Create a testable log sink
var buf bytes.Buffer
baseCore := zapcore.NewCore(
zapcore.NewJSONEncoder(defaultEncoderConfig()),
zapcore.Lock(zapcore.AddSync(&buf)),
globalLogLevel)
// Arrange: Set up a core stack
core := &suppressCallerCore{baseCore}
// Arrange: Create a logger
logger := zapr.NewLogger(zap.New(core, zap.AddCaller()))
// Act
logger.Info("message")
// Assert that caller information was suppressed
msg := buf.String()
ts.Assert().NotContains(msg, "caller")
}
func BenchmarkLoggerRedact(b *testing.B) {
msg := "this is a message with 'foo' in it"
logKvps := []any{"key", "value", "foo", "bar", "bar", "baz", "longval", "84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa"}
redactor := &dynamicRedactor{denySet: make(map[string]struct{})}
redactor.replacer.CompareAndSwap(nil, strings.NewReplacer())
b.Run("no redaction", func(b *testing.B) {
logger, flush := New("redaction-benchmark", WithJSONSink(
io.Discard,
func(conf *sinkConfig) { conf.redactor = redactor },
))
for i := 0; i < b.N; i++ {
logger.Info(msg, logKvps...)
}
require.NoError(b, flush())
})
b.Run("1 redaction", func(b *testing.B) {
logger, flush := New("redaction-benchmark", WithJSONSink(
io.Discard,
func(conf *sinkConfig) { conf.redactor = redactor },
))
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
for i := 0; i < b.N; i++ {
logger.Info(msg, logKvps...)
}
require.NoError(b, flush())
})
b.Run("2 redactions", func(b *testing.B) {
logger, flush := New("redaction-benchmark", WithJSONSink(
io.Discard,
func(conf *sinkConfig) { conf.redactor = redactor },
))
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
redactor.configureForRedaction("foo")
for i := 0; i < b.N; i++ {
logger.Info(msg, logKvps...)
}
require.NoError(b, flush())
})
b.Run("3 redactions", func(b *testing.B) {
logger, flush := New("redaction-benchmark", WithJSONSink(
io.Discard,
func(conf *sinkConfig) { conf.redactor = redactor },
))
redactor.configureForRedaction("84hblnqwp97ewilbgoab8fhqlngahs6dl3i269haa")
redactor.configureForRedaction("foo")
redactor.configureForRedaction("bar")
for i := 0; i < b.N; i++ {
logger.Info(msg, logKvps...)
}
require.NoError(b, flush())
})
}