Skip to content

Commit

Permalink
Merge branch 'main' into captaindata-v3-detector
Browse files Browse the repository at this point in the history
  • Loading branch information
kashifkhan0771 authored Oct 29, 2024
2 parents 9f39a91 + f42f632 commit 592dc6b
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 60 deletions.
14 changes: 7 additions & 7 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,10 @@ import (
"github.com/trufflesecurity/trufflehog/v3/pkg/config"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/engine"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
"github.com/trufflesecurity/trufflehog/v3/pkg/handlers"
"github.com/trufflesecurity/trufflehog/v3/pkg/log"
"github.com/trufflesecurity/trufflehog/v3/pkg/output"
"github.com/trufflesecurity/trufflehog/v3/pkg/feature"
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
"github.com/trufflesecurity/trufflehog/v3/pkg/tui"
"github.com/trufflesecurity/trufflehog/v3/pkg/updater"
Expand Down Expand Up @@ -72,10 +72,10 @@ var (
jobReportFile = cli.Flag("output-report", "Write a scan report to the provided path.").Hidden().OpenFile(os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0666)

// Add feature flags
forceSkipBinaries = cli.Flag("force-skip-binaries", "Force skipping binaries.").Bool()
forceSkipArchives = cli.Flag("force-skip-archives", "Force skipping archives.").Bool()
forceSkipBinaries = cli.Flag("force-skip-binaries", "Force skipping binaries.").Bool()
forceSkipArchives = cli.Flag("force-skip-archives", "Force skipping archives.").Bool()
skipAdditionalRefs = cli.Flag("skip-additional-refs", "Skip additional references.").Bool()
userAgentSuffix = cli.Flag("user-agent-suffix", "Suffix to add to User-Agent.").String()
userAgentSuffix = cli.Flag("user-agent-suffix", "Suffix to add to User-Agent.").String()

gitScan = cli.Command("git", "Find credentials in git repositories.")
gitScanURI = gitScan.Arg("uri", "Git repository URL. https://, file://, or ssh:// schema expected.").Required().String()
Expand Down Expand Up @@ -285,7 +285,7 @@ func main() {
if *jsonOut {
logFormat = log.WithJSONSink
}
logger, sync := log.New("trufflehog", logFormat(os.Stderr))
logger, sync := log.New("trufflehog", logFormat(os.Stderr, log.WithGlobalRedaction()))
// make it the default logger for contexts
context.SetDefaultLogger(logger)

Expand Down Expand Up @@ -375,15 +375,15 @@ func run(state overseer.State) {
}()
}

// Set feature configurations from CLI flags
// Set feature configurations from CLI flags
if *forceSkipBinaries {
feature.ForceSkipBinaries.Store(true)
}

if *forceSkipArchives {
feature.ForceSkipArchives.Store(true)
}

if *skipAdditionalRefs {
feature.SkipAdditionalRefs.Store(true)
}
Expand Down
41 changes: 0 additions & 41 deletions pkg/log/core.go

This file was deleted.

50 changes: 50 additions & 0 deletions pkg/log/dynamic_redactor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package log

import (
"strings"
"sync"
"sync/atomic"
)

type dynamicRedactor struct {
denySet map[string]struct{}
denySlice []string
denyMu sync.Mutex

replacer atomic.Pointer[strings.Replacer]
}

var globalRedactor *dynamicRedactor

func init() {
globalRedactor = &dynamicRedactor{denySet: make(map[string]struct{})}
globalRedactor.replacer.CompareAndSwap(nil, strings.NewReplacer())
}

// RedactGlobally configures the global log redactor to redact the provided value during log emission. The value will be
// redacted in log messages and values that are strings, but not in log keys or values of other types.
func RedactGlobally(sensitiveValue string) {
globalRedactor.configureForRedaction(sensitiveValue)
}

func (r *dynamicRedactor) configureForRedaction(sensitiveValue string) {
if sensitiveValue == "" {
return
}

r.denyMu.Lock()
defer r.denyMu.Unlock()

if _, ok := r.denySet[sensitiveValue]; ok {
return
}

r.denySet[sensitiveValue] = struct{}{}
r.denySlice = append(r.denySlice, sensitiveValue, "*****")

r.replacer.Store(strings.NewReplacer(r.denySlice...))
}

func (r *dynamicRedactor) redact(s string) string {
return r.replacer.Load().Replace(s)
}
1 change: 0 additions & 1 deletion pkg/log/level.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"go.uber.org/zap/zapcore"
)

// TODO: Use a struct to make testing easier.
var (
// Global, default log level control.
globalLogLevel levelSetter = zap.NewAtomicLevel()
Expand Down
30 changes: 21 additions & 9 deletions pkg/log/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,9 +83,10 @@ func WithSentry(opts sentry.ClientOptions, tags map[string]string) logConfig {
}

type sinkConfig struct {
encoder zapcore.Encoder
sink zapcore.WriteSyncer
level levelSetter
encoder zapcore.Encoder
sink zapcore.WriteSyncer
level levelSetter
redactor *dynamicRedactor
}

// WithJSONSink adds a JSON encoded output to the logger.
Expand Down Expand Up @@ -176,6 +177,13 @@ func WithLeveler(leveler levelSetter) func(*sinkConfig) {
}
}

// WithGlobalRedaction adds values to be redacted from logs.
func WithGlobalRedaction() func(*sinkConfig) {
return func(conf *sinkConfig) {
conf.redactor = globalRedactor
}
}

// firstErrorFunc is a helper function that returns a function that executes
// all provided args and returns the first error, if any.
func firstErrorFunc(fs ...func() error) func() error {
Expand Down Expand Up @@ -209,11 +217,15 @@ func newCoreConfig(
for _, f := range opts {
f(&conf)
}
return logConfig{
core: zapcore.NewCore(
conf.encoder,
conf.sink,
conf.level,
),
core := zapcore.NewCore(
conf.encoder,
conf.sink,
conf.level,
)

if conf.redactor == nil {
return logConfig{core: core}
}

return logConfig{core: NewRedactionCore(core, conf.redactor)}
}
125 changes: 123 additions & 2 deletions pkg/log/log_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import (

"github.com/getsentry/sentry-go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
)

func TestNew(t *testing.T) {
var jsonBuffer, consoleBuffer bytes.Buffer
logger, flush := New("service-name",
WithJSONSink(&jsonBuffer),
WithConsoleSink(&consoleBuffer),
WithJSONSink(&jsonBuffer, WithGlobalRedaction()),
WithConsoleSink(&consoleBuffer, WithGlobalRedaction()),
)
logger.Info("yay")
assert.Nil(t, flush())
Expand Down Expand Up @@ -233,3 +234,123 @@ func TestFindLevel(t *testing.T) {
assert.Equal(t, i8, findLevel(logger))
}
}

func TestGlobalRedaction_Console(t *testing.T) {
oldState := globalRedactor
globalRedactor = &dynamicRedactor{
denySet: make(map[string]struct{}),
}
defer func() { globalRedactor = oldState }()

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"})
require.NoError(t, 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",
}
assert.Equal(t, wantParts, gotParts)
}

func TestGlobalRedaction_JSON(t *testing.T) {
oldState := globalRedactor
globalRedactor = &dynamicRedactor{
denySet: make(map[string]struct{}),
}
defer func() { globalRedactor = oldState }()

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"})
require.NoError(t, flush())

var parsedJSON map[string]any
require.NoError(t, json.Unmarshal(jsonBuffer.Bytes(), &parsedJSON))
assert.NotEmpty(t, parsedJSON["ts"])
delete(parsedJSON, "ts")
assert.Equal(t,
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,
)
}

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())
})
}
42 changes: 42 additions & 0 deletions pkg/log/redaction_core.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package log

import (
"go.uber.org/zap/zapcore"
)

// redactionCore wraps a zapcore.Core to perform redaction of log messages in
// the message and field values.
type redactionCore struct {
zapcore.Core
redactor *dynamicRedactor
}

// NewRedactionCore creates a zapcore.Core that performs redaction of logs in
// the message and field values.
func NewRedactionCore(core zapcore.Core, redactor *dynamicRedactor) zapcore.Core {
return &redactionCore{core, redactor}
}

// Check overrides the embedded zapcore.Core Check() method to add the
// redactionCore to the zapcore.CheckedEntry.
func (c *redactionCore) Check(ent zapcore.Entry, ce *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.Enabled(ent.Level) {
return ce.AddCore(ent, c)
}
return ce
}

func (c *redactionCore) With(fields []zapcore.Field) zapcore.Core {
return NewRedactionCore(c.Core.With(fields), c.redactor)
}

// Write overrides the embedded zapcore.Core Write() method to redact the message and fields before passing them to be
// written. Only message and string values are redacted; keys and non-string values (e.g. those inside of arrays and
// structured objects) are not redacted.
func (c *redactionCore) Write(ent zapcore.Entry, fields []zapcore.Field) error {
ent.Message = c.redactor.redact(ent.Message)
for i := range fields {
fields[i].String = c.redactor.redact(fields[i].String)
}
return c.Core.Write(ent, fields)
}
Loading

0 comments on commit 592dc6b

Please sign in to comment.