Compare commits

...

5 Commits

Author SHA1 Message Date
Ahrav Dutta d58ff5b6fd remove mutex usage around failFast. 2023-09-08 11:43:17 -07:00
Ahrav Dutta c9c0a7f9bf replace channel w/ bool. 2023-09-07 19:52:19 -07:00
Ahrav Dutta 64880385c4 refctor. 2023-09-07 19:42:34 -07:00
Ahrav Dutta 6195932842 update comment. 2023-09-07 19:14:08 -07:00
Ahrav Dutta 641a0df72d Allow failing fast. 2023-09-07 19:07:03 -07:00
6 changed files with 147 additions and 55 deletions
+11 -6
View File
@@ -53,6 +53,7 @@ var (
printAvgDetectorTime = cli.Flag("print-avg-detector-time", "Print the average time spent on each detector.").Bool()
noUpdate = cli.Flag("no-update", "Don't check for updates.").Bool()
fail = cli.Flag("fail", "Exit with code 183 if results are found.").Bool()
failFast = cli.Flag("fail-fast", "Exit with code 1 as soon as a single result is found.").Bool()
verifiers = cli.Flag("verifier", "Set custom verification endpoints.").StringMap()
archiveMaxSize = cli.Flag("archive-max-size", "Maximum size of archive to scan. (Byte units eg. 512B, 2KB, 4MB)").Bytes()
archiveMaxDepth = cli.Flag("archive-max-depth", "Maximum depth of archive to scan.").Int()
@@ -105,11 +106,11 @@ var (
filesystemScanIncludePaths = filesystemScan.Flag("include-paths", "Path to file with newline separated regexes for files to include in scan.").Short('i').String()
filesystemScanExcludePaths = filesystemScan.Flag("exclude-paths", "Path to file with newline separated regexes for files to exclude in scan.").Short('x').String()
s3Scan = cli.Command("s3", "Find credentials in S3 buckets.")
s3ScanKey = s3Scan.Flag("key", "S3 key used to authenticate. Can be provided with environment variable AWS_ACCESS_KEY_ID.").Envar("AWS_ACCESS_KEY_ID").String()
s3ScanRoleArns = s3Scan.Flag("role-arn", "Specify the ARN of an IAM role to assume for scanning. You can repeat this flag.").Strings()
s3ScanSecret = s3Scan.Flag("secret", "S3 secret used to authenticate. Can be provided with environment variable AWS_SECRET_ACCESS_KEY.").Envar("AWS_SECRET_ACCESS_KEY").String()
s3ScanSessionToken = s3Scan.Flag("session-token", "S3 session token used to authenticate temporary credentials. Can be provided with environment variable AWS_SESSION_TOKEN.").Envar("AWS_SESSION_TOKEN").String()
s3Scan = cli.Command("s3", "Find credentials in S3 buckets.")
s3ScanKey = s3Scan.Flag("key", "S3 key used to authenticate. Can be provided with environment variable AWS_ACCESS_KEY_ID.").Envar("AWS_ACCESS_KEY_ID").String()
s3ScanRoleArns = s3Scan.Flag("role-arn", "Specify the ARN of an IAM role to assume for scanning. You can repeat this flag.").Strings()
s3ScanSecret = s3Scan.Flag("secret", "S3 secret used to authenticate. Can be provided with environment variable AWS_SECRET_ACCESS_KEY.").Envar("AWS_SECRET_ACCESS_KEY").String()
s3ScanSessionToken = s3Scan.Flag("session-token", "S3 session token used to authenticate temporary credentials. Can be provided with environment variable AWS_SESSION_TOKEN.").Envar("AWS_SESSION_TOKEN").String()
s3ScanCloudEnv = s3Scan.Flag("cloud-environment", "Use IAM credentials in cloud environment.").Bool()
s3ScanBuckets = s3Scan.Flag("bucket", "Name of S3 bucket to scan. You can repeat this flag.").Strings()
s3ScanMaxObjectSize = s3Scan.Flag("max-object-size", "Maximum size of objects to scan. Objects larger than this will be skipped. (Byte units eg. 512B, 2KB, 4MB)").Default("250MB").Bytes()
@@ -366,6 +367,7 @@ func run(state overseer.State) {
engine.WithOnlyVerified(*onlyVerified),
engine.WithPrintAvgDetectorTime(*printAvgDetectorTime),
engine.WithPrinter(printer),
engine.WithFailFast(*failFast),
)
if err != nil {
logFatal(err, "error initializing engine")
@@ -469,7 +471,7 @@ func run(state overseer.State) {
Secret: *s3ScanSecret,
SessionToken: *s3ScanSessionToken,
Buckets: *s3ScanBuckets,
Roles: *s3ScanRoleArns,
Roles: *s3ScanRoleArns,
CloudCred: *s3ScanCloudEnv,
MaxObjectSize: int64(*s3ScanMaxObjectSize),
}
@@ -533,6 +535,9 @@ func run(state overseer.State) {
if err = e.Finish(ctx); err != nil {
logFatal(err, "engine failed to finish execution")
}
if e.IsTerminated() {
os.Exit(1)
}
metrics := e.GetMetrics()
// Print results.
+132 -33
View File
@@ -51,6 +51,53 @@ type Printer interface {
Print(ctx context.Context, r *detectors.ResultWithMetadata) error
}
// terminationSignal provides a mechanism to safely signal a termination
// event across different goroutines. It uses a combination of a condition variable
// and a mutex to enable safe signaling and waiting, ensuring that the signaling
// is consistent and that there are no race conditions when checking or updating
// the status of the termination.
type terminationSignal struct {
// isTerminated indicates the signal state (true if the engine is terminated).
isTerminated bool
// isFailFast indicates if the engine is in fail-fast mode.
isFailFast bool
// mutex is used to protect access to isFailFast and to synchronize
// the condition variable.
mutex sync.Mutex
// failFastCondition is a condition variable that's signaled when
// a fail-fast event occurs. It's used in Finish to wait for or
// react to this signal.
failFastCondition *sync.Cond
}
// newTerminationSignal initializes a new terminationSignal.
// The failFast argument determines the initial state.
func newTerminationSignal(failFast bool) *terminationSignal {
ts := &terminationSignal{isFailFast: failFast}
// Initialize the condition variable with the mutex.
ts.failFastCondition = sync.NewCond(&ts.mutex)
return ts
}
// signalFinish signals that the engine has finished its work.
// It uses the condition variable to notify any waiting goroutines
// (e.g., the Finish method).
func (t *terminationSignal) signalFinish() {
t.mutex.Lock()
defer t.mutex.Unlock()
t.isTerminated = true
t.failFastCondition.Signal() // Signal the condition to unblock Finish.
}
func (t *terminationSignal) wait() {
t.mutex.Lock()
defer t.mutex.Unlock()
for !t.isTerminated {
t.failFastCondition.Wait()
}
}
type Engine struct {
// CLI flags.
concurrency uint8
@@ -74,6 +121,11 @@ type Engine struct {
workersWg sync.WaitGroup
wgDetectorWorkers sync.WaitGroup
WgNotifier sync.WaitGroup
// termSignal is used to signal the engine to terminate.
// It is used to implement the fail-fast feature,
// where the engine will terminate scanning as soon as
// the first potential secret is detected.
termSignal *terminationSignal
// Runtime information.
metrics runtimeMetrics
@@ -169,6 +221,19 @@ func WithPrinter(printer Printer) EngineOption {
}
}
// WithFailFast returns an EngineOption that configures the engine's termination behavior.
// If 'failFast' is enabled, the engine will terminate scanning upon detecting the first potential secret.
// This mode is valuable in contexts like CI/CD pipelines or preliminary scans where
// immediate feedback is more important than a comprehensive review. In the non-failFast mode,
// the engine continues its operation without waiting for a result.
// The boolean parameter 'failFast' dictates the termination behavior.
func WithFailFast(failFast bool) EngineOption {
return func(e *Engine) {
// Configure the termination signal with the desired mode (failFast or not).
e.termSignal = newTerminationSignal(failFast)
}
}
func filterDetectors(filterFunc func(detectors.Detector) bool, input []detectors.Detector) []detectors.Detector {
var output []detectors.Detector
for _, detector := range input {
@@ -276,6 +341,7 @@ func Start(ctx context.Context, options ...EngineOption) (*Engine, error) {
dedupeCache: cache,
printer: new(output.PlainPrinter), // default printer
metrics: runtimeMetrics{Metrics: Metrics{scanStartTime: time.Now()}},
termSignal: &terminationSignal{},
}
for _, option := range options {
@@ -367,22 +433,15 @@ func Start(ctx context.Context, options ...EngineOption) (*Engine, error) {
}()
}
// We want 1/4th of the notifier workers as the number of scanner workers.
const notifierWorkerRatio = 4
maxNotifierWorkers := 1
if numWorkers := e.concurrency / notifierWorkerRatio; numWorkers > 0 {
maxNotifierWorkers = int(numWorkers)
}
ctx.Logger().V(2).Info("starting notifier workers", "count", maxNotifierWorkers)
for worker := 0; worker < maxNotifierWorkers; worker++ {
e.WgNotifier.Add(1)
go func() {
ctx := context.WithValue(ctx, "notifier_worker_id", common.RandomID(5))
defer common.Recover(ctx)
defer e.WgNotifier.Done()
e.notifyResults(ctx)
}()
}
// We want only one notifier worker.
ctx.Logger().V(2).Info("starting notifier worker")
e.WgNotifier.Add(1)
go func() {
ctx := context.WithValue(ctx, "notifier_worker_id", common.RandomID(5))
defer common.Recover(ctx)
defer e.WgNotifier.Done()
e.notifyResults(ctx)
}()
return e, nil
}
@@ -392,6 +451,13 @@ func Start(ctx context.Context, options ...EngineOption) (*Engine, error) {
// more sources may be scanned by the engine.
func (e *Engine) Finish(ctx context.Context) error {
defer common.RecoverWithExit(ctx)
// Handle the fail-fast termination signal.
if e.termSignal.isFailFast {
e.termSignal.wait()
return fmt.Errorf("terminated early due to fail-fast flag after printing the first result")
}
// Wait for the sources to finish putting chunks onto the chunks channel.
err := e.sourceManager.Wait()
@@ -407,6 +473,15 @@ func (e *Engine) Finish(ctx context.Context) error {
return err
}
// IsTerminated checks if the engine received a termination signal.
// This can be used, for example, to determine if the engine
// terminated early due to the fail-fast feature being activated.
func (e *Engine) IsTerminated() bool {
e.termSignal.mutex.Lock()
defer e.termSignal.mutex.Unlock()
return e.termSignal.isTerminated
}
func (e *Engine) ChunksChan() <-chan *sources.Chunk {
return e.sourceManager.Chunks()
}
@@ -559,27 +634,51 @@ func (e *Engine) processResult(data detectableChunk, res detectors.Result) {
}
func (e *Engine) notifyResults(ctx context.Context) {
if e.termSignal.isFailFast {
e.processResultsWithFailFast(ctx)
} else {
e.processRegularResults(ctx)
}
}
func (e *Engine) processResultsWithFailFast(ctx context.Context) {
for r := range e.ResultsChan() {
if e.onlyVerified && !r.Verified {
continue
}
atomic.AddUint32(&e.numFoundResults, 1)
e.processSingleResult(ctx, &r)
key := fmt.Sprintf("%s%s%s%+v", r.DetectorType.String(), r.Raw, r.RawV2, r.SourceMetadata)
if _, ok := e.dedupeCache.Get(key); ok {
continue
}
e.dedupeCache.Add(key, struct{}{})
// If fail-fast is enabled, signal finish after processing the first result.
e.termSignal.signalFinish()
return
}
}
if r.Verified {
atomic.AddUint64(&e.metrics.VerifiedSecretsFound, 1)
} else {
atomic.AddUint64(&e.metrics.UnverifiedSecretsFound, 1)
}
func (e *Engine) processRegularResults(ctx context.Context) {
for r := range e.ResultsChan() {
e.processSingleResult(ctx, &r)
}
}
if err := e.printer.Print(ctx, &r); err != nil {
ctx.Logger().Error(err, "error printing result")
}
func (e *Engine) processSingleResult(ctx context.Context, r *detectors.ResultWithMetadata) {
if e.onlyVerified && !r.Verified {
return
}
atomic.AddUint32(&e.numFoundResults, 1)
key := fmt.Sprintf("%s%s%s%+v", r.DetectorType.String(), r.Raw, r.RawV2, r.SourceMetadata)
if _, ok := e.dedupeCache.Get(key); ok {
return
}
e.dedupeCache.Add(key, struct{}{})
if r.Verified {
atomic.AddUint64(&e.metrics.VerifiedSecretsFound, 1)
} else {
atomic.AddUint64(&e.metrics.UnverifiedSecretsFound, 1)
}
if err := e.printer.Print(ctx, r); err != nil {
ctx.Logger().Error(err, "error printing result")
}
}
+1 -4
View File
@@ -4,7 +4,6 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"sync"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
@@ -14,7 +13,7 @@ import (
var dedupeCache = make(map[string]struct{})
// GitHubActionsPrinter is a printer that prints results in GitHub Actions format.
type GitHubActionsPrinter struct{ mu sync.Mutex }
type GitHubActionsPrinter struct{}
func (p *GitHubActionsPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata) error {
out := gitHubActionsOutputFormat{
@@ -57,8 +56,6 @@ func (p *GitHubActionsPrinter) Print(_ context.Context, r *detectors.ResultWithM
}
dedupeCache[key] = struct{}{}
p.mu.Lock()
defer p.mu.Unlock()
message := fmt.Sprintf("Found %s %s result 🐷🔑\n", verifiedStatus, out.DetectorType)
if r.Result.DecoderType != detectorspb.DecoderType_PLAIN {
message = fmt.Sprintf("Found %s %s result with %s encoding 🐷🔑\n", verifiedStatus, out.DetectorType, out.DecoderType)
+1 -4
View File
@@ -3,7 +3,6 @@ package output
import (
"encoding/json"
"fmt"
"sync"
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
@@ -13,7 +12,7 @@ import (
)
// JSONPrinter is a printer that prints results in JSON format.
type JSONPrinter struct{ mu sync.Mutex }
type JSONPrinter struct{}
func (p *JSONPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata) error {
v := &struct {
@@ -62,8 +61,6 @@ func (p *JSONPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata)
return fmt.Errorf("could not marshal result: %w", err)
}
p.mu.Lock()
fmt.Println(string(out))
p.mu.Unlock()
return nil
}
+1 -4
View File
@@ -7,7 +7,6 @@ import (
"net/url"
"os"
"strings"
"sync"
gogit "github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
@@ -21,7 +20,7 @@ import (
)
// LegacyJSONPrinter is a printer that prints results in legacy JSON format for backwards compatibility.
type LegacyJSONPrinter struct{ mu sync.Mutex }
type LegacyJSONPrinter struct{}
func (p *LegacyJSONPrinter) Print(ctx context.Context, r *detectors.ResultWithMetadata) error {
var repo string
@@ -54,9 +53,7 @@ func (p *LegacyJSONPrinter) Print(ctx context.Context, r *detectors.ResultWithMe
return fmt.Errorf("could not marshal result: %w", err)
}
p.mu.Lock()
fmt.Println(string(out))
p.mu.Unlock()
return nil
}
+1 -4
View File
@@ -5,7 +5,6 @@ import (
"fmt"
"sort"
"strings"
"sync"
"github.com/fatih/color"
"golang.org/x/text/cases"
@@ -23,7 +22,7 @@ var (
)
// PlainPrinter is a printer that prints results in plain text format.
type PlainPrinter struct{ mu sync.Mutex }
type PlainPrinter struct{}
func (p *PlainPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata) error {
out := outputFormat{
@@ -40,8 +39,6 @@ func (p *PlainPrinter) Print(_ context.Context, r *detectors.ResultWithMetadata)
}
printer := greenPrinter
p.mu.Lock()
defer p.mu.Unlock()
if out.Verified {
yellowPrinter.Print("Found verified result 🐷🔑\n")