Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d58ff5b6fd | |||
| c9c0a7f9bf | |||
| 64880385c4 | |||
| 6195932842 | |||
| 641a0df72d |
@@ -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
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user