Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d58ff5b6fd | |||
| c9c0a7f9bf | |||
| 64880385c4 | |||
| 6195932842 | |||
| 641a0df72d | |||
| 8d66fde6de | |||
| 2a9f34962d |
@@ -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()
|
||||
@@ -136,12 +137,6 @@ var (
|
||||
circleCiScan = cli.Command("circleci", "Scan CircleCI")
|
||||
circleCiScanToken = circleCiScan.Flag("token", "CircleCI token. Can also be provided with environment variable").Envar("CIRCLECI_TOKEN").Required().String()
|
||||
|
||||
trelloScan = cli.Command("trello", "Scan Trello")
|
||||
trelloApiKey = trelloScan.Flag("apiKey", "Trello API Key. Can also be provided with environment variable").Envar("TRELLO_API_KEY").Required().String()
|
||||
trelloToken = trelloScan.Flag("token", "Trello token. Can also be provided with environment variable").Envar("TRELLO_TOKEN").Required().String()
|
||||
trelloBoardIDs = trelloScan.Flag("boardIDs", "Trello board IDs to scan. Can also be provided with environment variable").Envar("TRELLO_BOARD_IDS").Required().Strings()
|
||||
|
||||
|
||||
dockerScan = cli.Command("docker", "Scan Docker Image")
|
||||
dockerScanImages = dockerScan.Flag("image", "Docker image to scan. Use the file:// prefix to point to a local tarball, otherwise a image registry is assumed.").Required().Strings()
|
||||
)
|
||||
@@ -372,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")
|
||||
@@ -475,7 +471,7 @@ func run(state overseer.State) {
|
||||
Secret: *s3ScanSecret,
|
||||
SessionToken: *s3ScanSessionToken,
|
||||
Buckets: *s3ScanBuckets,
|
||||
Roles: *s3ScanRoleArns,
|
||||
Roles: *s3ScanRoleArns,
|
||||
CloudCred: *s3ScanCloudEnv,
|
||||
MaxObjectSize: int64(*s3ScanMaxObjectSize),
|
||||
}
|
||||
@@ -498,10 +494,6 @@ func run(state overseer.State) {
|
||||
if err := e.ScanCircleCI(ctx, *circleCiScanToken); err != nil {
|
||||
logFatal(err, "Failed to scan CircleCI.")
|
||||
}
|
||||
case trelloScan.FullCommand():
|
||||
if err := e.ScanTrello(ctx, *trelloBoardIDs, *trelloApiKey, *trelloToken); err != nil {
|
||||
logFatal(err, "Failed to scan Trello.")
|
||||
}
|
||||
case gcsScan.FullCommand():
|
||||
cfg := sources.GCSConfig{
|
||||
ProjectID: *gcsProjectID,
|
||||
@@ -543,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.
|
||||
|
||||
@@ -13,13 +13,15 @@ import (
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
)
|
||||
|
||||
type Scanner struct{}
|
||||
type Scanner struct {
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// Ensure the Scanner satisfies the interface at compile time.
|
||||
var _ detectors.Detector = (*Scanner)(nil)
|
||||
|
||||
var (
|
||||
client = common.SaneHttpClient()
|
||||
defaultClient = common.SaneHttpClient()
|
||||
|
||||
keyPat = regexp.MustCompile(detectors.PrefixRegex([]string{"sendgrid"}) + `(SG\.[\w\-_]{20,24}\.[\w\-_]{39,50})\b`)
|
||||
)
|
||||
@@ -42,7 +44,7 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
}
|
||||
res := strings.TrimSpace(match[1])
|
||||
|
||||
s := detectors.Result{
|
||||
s1 := detectors.Result{
|
||||
DetectorType: detectorspb.DetectorType_SendGrid,
|
||||
Raw: []byte(res),
|
||||
}
|
||||
@@ -52,6 +54,11 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
// 403 will be issued if the scope is wrong but the key is correct
|
||||
baseURL := "https://api.sendgrid.com/v3/templates"
|
||||
|
||||
client := s.client
|
||||
if client == nil {
|
||||
client = defaultClient
|
||||
}
|
||||
|
||||
// test `read_user` scope
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", baseURL, nil)
|
||||
if err != nil {
|
||||
@@ -67,16 +74,18 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result
|
||||
// 403 means good key but not the right scope
|
||||
// 401 is bad key
|
||||
if res.StatusCode == http.StatusOK || res.StatusCode == http.StatusForbidden {
|
||||
s.Verified = true
|
||||
s1.Verified = true
|
||||
} else if res.StatusCode != http.StatusUnauthorized {
|
||||
s1.VerificationError = fmt.Errorf("unexpected HTTP response status %d", res.StatusCode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !s.Verified && detectors.IsKnownFalsePositive(string(s.Raw), detectors.DefaultFalsePositives, true) {
|
||||
if !s1.Verified && detectors.IsKnownFalsePositive(string(s1.Raw), detectors.DefaultFalsePositives, true) {
|
||||
continue
|
||||
}
|
||||
|
||||
results = append(results, s)
|
||||
results = append(results, s1)
|
||||
}
|
||||
|
||||
return
|
||||
|
||||
@@ -9,7 +9,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/detectors"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/detectorspb"
|
||||
@@ -18,7 +19,7 @@ import (
|
||||
func TestSendgrid_FromChunk(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2")
|
||||
testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors5")
|
||||
if err != nil {
|
||||
t.Fatalf("could not get test secrets from GCP: %s", err)
|
||||
}
|
||||
@@ -30,11 +31,12 @@ func TestSendgrid_FromChunk(t *testing.T) {
|
||||
verify bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
name string
|
||||
s Scanner
|
||||
args args
|
||||
want []detectors.Result
|
||||
wantErr bool
|
||||
wantVerificationErr bool
|
||||
}{
|
||||
{
|
||||
name: "found, verified",
|
||||
@@ -50,7 +52,8 @@ func TestSendgrid_FromChunk(t *testing.T) {
|
||||
Verified: true,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, unverified",
|
||||
@@ -66,7 +69,8 @@ func TestSendgrid_FromChunk(t *testing.T) {
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "not found",
|
||||
@@ -76,22 +80,54 @@ func TestSendgrid_FromChunk(t *testing.T) {
|
||||
data: []byte("You cannot find the secret within"),
|
||||
verify: true,
|
||||
},
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
want: nil,
|
||||
wantErr: false,
|
||||
wantVerificationErr: false,
|
||||
},
|
||||
{
|
||||
name: "found, would be verified if not for timeout",
|
||||
s: Scanner{client: common.SaneHttpClientTimeOut(1 * time.Microsecond)},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a sendgrid secret %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_SendGrid,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
{
|
||||
name: "found, verified but unexpected api surface",
|
||||
s: Scanner{client: common.ConstantResponseHttpClient(404, "")},
|
||||
args: args{
|
||||
ctx: context.Background(),
|
||||
data: []byte(fmt.Sprintf("You can find a sendgrid secret %s within", secret)),
|
||||
verify: true,
|
||||
},
|
||||
want: []detectors.Result{
|
||||
{
|
||||
DetectorType: detectorspb.DetectorType_SendGrid,
|
||||
Verified: false,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
wantVerificationErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := Scanner{}
|
||||
got, err := s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Sendgrid.FromData() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
for i := range got {
|
||||
got[i].Raw = nil
|
||||
}
|
||||
if diff := pretty.Compare(got, tt.want); diff != "" {
|
||||
ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "VerificationError")
|
||||
if diff := cmp.Diff(got, tt.want, ignoreOpts); diff != "" {
|
||||
t.Errorf("Sendgrid.FromData() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
|
||||
+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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources/trello"
|
||||
)
|
||||
|
||||
// ScanTrello scans Trello boards for secrets.
|
||||
func (e *Engine) ScanTrello(ctx context.Context, apiKey string) error {
|
||||
trelloSource := trello.Source{}
|
||||
boardIDs, err := trelloSource.GetBoardIDs(ctx, apiKey)
|
||||
if err != nil {
|
||||
ctx.Logger().Error(err, "failed to get Trello board IDs")
|
||||
return err
|
||||
}
|
||||
|
||||
connection := &sourcespb.Trello{
|
||||
Auth: &sourcespb.Trello_TrelloAuth{
|
||||
ApiKey: apiKey,
|
||||
},
|
||||
Boards: boardIDs,
|
||||
}
|
||||
|
||||
var conn anypb.Any
|
||||
err = anypb.MarshalFrom(&conn, connection, proto.MarshalOptions{})
|
||||
if err != nil {
|
||||
ctx.Logger().Error(err, "failed to marshal Trello connection")
|
||||
return err
|
||||
}
|
||||
|
||||
handle, err := e.sourceManager.Enroll(ctx, "trufflehog - Trello", new(trello.Source).Type(),
|
||||
func(ctx context.Context, jobID, sourceID int64) (sources.Source, error) {
|
||||
if err := trelloSource.Init(ctx, "trufflehog - Trello", jobID, sourceID, true, &conn, runtime.NumCPU()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &trelloSource, nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = e.sourceManager.ScheduleRun(ctx, handle)
|
||||
return err
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -60,7 +60,6 @@ const (
|
||||
SourceType_SOURCE_TYPE_SHAREPOINT SourceType = 29
|
||||
SourceType_SOURCE_TYPE_GCS_UNAUTHED SourceType = 30
|
||||
SourceType_SOURCE_TYPE_AZURE_REPOS SourceType = 31
|
||||
SourceType_SOURCE_TYPE_TRELLO SourceType = 32
|
||||
)
|
||||
|
||||
// Enum value maps for SourceType.
|
||||
@@ -98,7 +97,6 @@ var (
|
||||
29: "SOURCE_TYPE_SHAREPOINT",
|
||||
30: "SOURCE_TYPE_GCS_UNAUTHED",
|
||||
31: "SOURCE_TYPE_AZURE_REPOS",
|
||||
32: "SOURCE_TYPE_AZURE_TRELLO",
|
||||
}
|
||||
SourceType_value = map[string]int32{
|
||||
"SOURCE_TYPE_AZURE_STORAGE": 0,
|
||||
@@ -133,7 +131,6 @@ var (
|
||||
"SOURCE_TYPE_SHAREPOINT": 29,
|
||||
"SOURCE_TYPE_GCS_UNAUTHED": 30,
|
||||
"SOURCE_TYPE_AZURE_REPOS": 31,
|
||||
"SOURCE_TYPE_AZURE_TRELLO": 32,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -526,80 +523,6 @@ func (*Bitbucket_Oauth) isBitbucket_Credential() {}
|
||||
|
||||
func (*Bitbucket_BasicAuth) isBitbucket_Credential() {}
|
||||
|
||||
type Trello struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Auth *Trello_TrelloAuth `protobuf:"bytes,1,opt,name=auth,proto3,oneof"`
|
||||
Boards []string `protobuf:"bytes,2,rep,name=boards,proto3" json:"boards,omitempty"`
|
||||
}
|
||||
|
||||
type Trello_TrelloAuth struct {
|
||||
ApiKey string `protobuf:"bytes,1,opt,name=api_key,json=apiKey,proto3,oneof"`
|
||||
Token string `protobuf:"bytes,2,opt,name=token,proto3,oneof"`
|
||||
}
|
||||
|
||||
func (x *Trello) Reset() {
|
||||
*x = Trello{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_sources_proto_msgTypes[4]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *Trello) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*Trello) ProtoMessage() {}
|
||||
|
||||
func (x *Trello) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_sources_proto_msgTypes[4]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
// Deprecated: Use Trello.ProtoReflect.Descriptor instead.
|
||||
func (*Trello) Descriptor() ([]byte, []int) {
|
||||
return file_sources_proto_rawDescGZIP(), []int{4} // Assuming Trello is the 5th message in source.proto file
|
||||
}
|
||||
|
||||
func (x *Trello) GetAuth() *Trello_TrelloAuth {
|
||||
if x != nil {
|
||||
return x.Auth
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Trello_TrelloAuth) GetApiKey() string {
|
||||
if t != nil && t.ApiKey != nil {
|
||||
return t.ApiKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (t *Trello_TrelloAuth) GetToken() string {
|
||||
if t != nil && t.Token != nil {
|
||||
return t.Token
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *Trello) GetBoards() []string {
|
||||
if x != nil {
|
||||
return x.Boards
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
type CircleCI struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
@@ -3770,7 +3693,6 @@ var file_sources_proto_goTypes = []interface{}{
|
||||
(*SlackRealtime)(nil), // 28: sources.SlackRealtime
|
||||
(*Sharepoint)(nil), // 29: sources.Sharepoint
|
||||
(*AzureRepos)(nil), // 30: sources.AzureRepos
|
||||
(*Trello)(nil), // 45: sources.Trello
|
||||
(*durationpb.Duration)(nil), // 31: google.protobuf.Duration
|
||||
(*anypb.Any)(nil), // 32: google.protobuf.Any
|
||||
(*credentialspb.BasicAuth)(nil), // 33: credentials.BasicAuth
|
||||
@@ -4304,16 +4226,13 @@ func file_sources_proto_init() {
|
||||
(*AzureRepos_Token)(nil),
|
||||
(*AzureRepos_Oauth)(nil),
|
||||
}
|
||||
file_sources_proto_msgTypes[29].OneofWrappers = []interface{}{
|
||||
(*Trello_TrelloAuth)(nil),
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_sources_proto_rawDesc,
|
||||
NumEnums: 2,
|
||||
NumMessages: 30,
|
||||
NumMessages: 29,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
|
||||
@@ -75,7 +75,7 @@ func (s *Source) Init(_ context.Context, name string, jobId, sourceId int64, ver
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
projects, err := s.projects(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting projects: %w", err)
|
||||
|
||||
@@ -85,7 +85,7 @@ type layerInfo struct {
|
||||
}
|
||||
|
||||
// Chunks emits data over a channel that is decoded and scanned for secrets.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
ctx = context.WithValues(ctx, "source_type", s.Type(), "source_name", s.name)
|
||||
|
||||
workers := new(errgroup.Group)
|
||||
|
||||
@@ -77,7 +77,7 @@ func (s *Source) WithFilter(filter *common.Filter) {
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
for i, path := range s.paths {
|
||||
logger := ctx.Logger().WithValues("path", path)
|
||||
if common.IsDone(ctx) {
|
||||
|
||||
@@ -248,7 +248,7 @@ func (s *Source) enumerate(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
persistableCache := s.setupCache(ctx)
|
||||
|
||||
objectCh, err := s.gcsManager.ListObjects(ctx)
|
||||
|
||||
@@ -153,7 +153,7 @@ func (s *Source) Init(aCtx context.Context, name string, jobId, sourceId int64,
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
if err := s.scanRepos(ctx, chunksChan); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -413,7 +413,7 @@ func (s *Source) visibilityOf(ctx context.Context, repoURL string) (visibility s
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
apiEndpoint := s.conn.Endpoint
|
||||
if len(apiEndpoint) == 0 || endsWithGithub.MatchString(apiEndpoint) {
|
||||
apiEndpoint = "https://api.github.com"
|
||||
|
||||
@@ -139,7 +139,7 @@ func (s *Source) Init(_ context.Context, name string, jobId, sourceId int64, ver
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
// Start client.
|
||||
apiClient, err := s.newClient()
|
||||
if err != nil {
|
||||
|
||||
@@ -232,7 +232,7 @@ func (s *Source) scanBuckets(ctx context.Context, client *s3.S3, role string, bu
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
visitor := func(c context.Context, defaultRegionClient *s3.S3, roleArn string, buckets []string) {
|
||||
s.scanBuckets(c, defaultRegionClient, roleArn, buckets, chunksChan)
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
)
|
||||
|
||||
// DummySource implements Source and is used for testing a SourceManager.
|
||||
@@ -31,7 +32,7 @@ func (d *DummySource) GetProgress() *Progress { return nil }
|
||||
|
||||
// Interface to easily test different chunking methods.
|
||||
type chunker interface {
|
||||
Chunks(context.Context, chan *Chunk) error
|
||||
Chunks(context.Context, chan *Chunk, ...ChunkingTarget) error
|
||||
ChunkUnit(ctx context.Context, unit SourceUnit, reporter ChunkReporter) error
|
||||
Enumerate(ctx context.Context, reporter UnitReporter) error
|
||||
}
|
||||
@@ -42,7 +43,7 @@ type counterChunker struct {
|
||||
count int
|
||||
}
|
||||
|
||||
func (c *counterChunker) Chunks(ctx context.Context, ch chan *Chunk) error {
|
||||
func (c *counterChunker) Chunks(ctx context.Context, ch chan *Chunk, _ ...ChunkingTarget) error {
|
||||
for i := 0; i < c.count; i++ {
|
||||
select {
|
||||
case ch <- &Chunk{Data: []byte{c.chunkCounter}}:
|
||||
@@ -75,9 +76,9 @@ func (c *counterChunker) ChunkUnit(ctx context.Context, unit SourceUnit, reporte
|
||||
// Chunk method that always returns an error.
|
||||
type errorChunker struct{ error }
|
||||
|
||||
func (c errorChunker) Chunks(context.Context, chan *Chunk) error { return c }
|
||||
func (c errorChunker) Enumerate(context.Context, UnitReporter) error { return c }
|
||||
func (c errorChunker) ChunkUnit(context.Context, SourceUnit, ChunkReporter) error { return c }
|
||||
func (c errorChunker) Chunks(context.Context, chan *Chunk, ...ChunkingTarget) error { return c }
|
||||
func (c errorChunker) Enumerate(context.Context, UnitReporter) error { return c }
|
||||
func (c errorChunker) ChunkUnit(context.Context, SourceUnit, ChunkReporter) error { return c }
|
||||
|
||||
// enrollDummy is a helper function to enroll a DummySource with a SourceManager.
|
||||
func enrollDummy(mgr *SourceManager, chunkMethod chunker) (handle, error) {
|
||||
@@ -176,7 +177,7 @@ type unitChunk struct {
|
||||
|
||||
type unitChunker struct{ steps []unitChunk }
|
||||
|
||||
func (c *unitChunker) Chunks(ctx context.Context, ch chan *Chunk) error {
|
||||
func (c *unitChunker) Chunks(ctx context.Context, ch chan *Chunk, _ ...ChunkingTarget) error {
|
||||
for _, step := range c.steps {
|
||||
if step.err != "" {
|
||||
continue
|
||||
@@ -294,7 +295,9 @@ type callbackChunker struct {
|
||||
cb func(context.Context, chan *Chunk) error
|
||||
}
|
||||
|
||||
func (c callbackChunker) Chunks(ctx context.Context, ch chan *Chunk) error { return c.cb(ctx, ch) }
|
||||
func (c callbackChunker) Chunks(ctx context.Context, ch chan *Chunk, _ ...ChunkingTarget) error {
|
||||
return c.cb(ctx, ch)
|
||||
}
|
||||
func (c callbackChunker) Enumerate(context.Context, UnitReporter) error { return nil }
|
||||
func (c callbackChunker) ChunkUnit(context.Context, SourceUnit, ChunkReporter) error { return nil }
|
||||
|
||||
|
||||
+16
-2
@@ -30,6 +30,16 @@ type Chunk struct {
|
||||
Verify bool
|
||||
}
|
||||
|
||||
// ChunkingTarget specifies criteria for a targeted chunking process.
|
||||
// Instead of collecting data indiscriminately, this struct allows the caller
|
||||
// to specify particular subsets of data they're interested in. This becomes
|
||||
// especially useful when one needs to verify or recheck specific data points
|
||||
// without processing the entire dataset.
|
||||
type ChunkingTarget struct {
|
||||
// QueryCriteria represents specific parameters or conditions to target the chunking process.
|
||||
QueryCriteria source_metadatapb.MetaData
|
||||
}
|
||||
|
||||
// Source defines the interface required to implement a source chunker.
|
||||
type Source interface {
|
||||
// Type returns the source type, used for matching against configuration and jobs.
|
||||
@@ -40,8 +50,12 @@ type Source interface {
|
||||
JobID() int64
|
||||
// Init initializes the source.
|
||||
Init(aCtx context.Context, name string, jobId, sourceId int64, verify bool, connection *anypb.Any, concurrency int) error
|
||||
// Chunks emits data over a channel that is decoded and scanned for secrets.
|
||||
Chunks(ctx context.Context, chunksChan chan *Chunk) error
|
||||
// Chunks emits data over a channel which is then decoded and scanned for secrets.
|
||||
// By default, data is obtained indiscriminately. However, by providing one or more
|
||||
// ChunkingTarget parameters, the caller can direct the function to retrieve
|
||||
// specific chunks of data. This targeted approach allows for efficient and
|
||||
// intentional data processing, beneficial when verifying or rechecking specific data points.
|
||||
Chunks(ctx context.Context, chunksChan chan *Chunk, targets ...ChunkingTarget) error
|
||||
// GetProgress is the completion progress (percentage) for Scanned Source.
|
||||
GetProgress() *Progress
|
||||
}
|
||||
|
||||
@@ -182,7 +182,7 @@ func (s *Source) verifyConnectionConfig() error {
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk, _ ...sources.ChunkingTarget) error {
|
||||
switch {
|
||||
case s.conn.TlsCert != nilString || s.conn.TlsKey != nilString:
|
||||
cert, err := tls.X509KeyPair([]byte(s.conn.TlsCert), []byte(s.conn.TlsKey))
|
||||
|
||||
@@ -1,239 +0,0 @@
|
||||
package trello
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/go-errors/errors"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
|
||||
)
|
||||
|
||||
const baseURL = "https://api.trello.com/1/"
|
||||
|
||||
type Source struct {
|
||||
name string
|
||||
apiKey string
|
||||
token string
|
||||
boardIDs []string
|
||||
sourceId int64
|
||||
jobId int64
|
||||
verify bool
|
||||
jobPool *errgroup.Group
|
||||
sources.Progress
|
||||
client *http.Client
|
||||
sources.CommonSourceUnitUnmarshaller
|
||||
}
|
||||
|
||||
// Type returns the type of the source.
|
||||
func (s *Source) Type() sourcespb.SourceType {
|
||||
return sourcespb.SourceType_SOURCE_TYPE_TRELLO
|
||||
}
|
||||
|
||||
// Init returns an initialized Source.
|
||||
func (s *Source) Init(_ context.Context, name string, jobId, sourceId int64, verify bool, connection *anypb.Any, concurrency int) error {
|
||||
s.name = name
|
||||
s.sourceId = sourceId
|
||||
s.jobId = jobId
|
||||
s.verify = verify
|
||||
s.jobPool = &errgroup.Group{}
|
||||
s.jobPool.SetLimit(concurrency)
|
||||
s.client = common.RetryableHttpClientTimeout(3)
|
||||
|
||||
var conn sourcespb.Trello
|
||||
if err := anypb.UnmarshalTo(connection, &conn, proto.UnmarshalOptions{}); err != nil {
|
||||
return errors.WrapPrefix(err, "error unmarshalling connection", 0)
|
||||
}
|
||||
|
||||
s.apiKey = conn.GetAuth().GetApiKey()
|
||||
s.token = conn.GetAuth().GetToken()
|
||||
s.boardIDs = conn.GetBoards()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Chunks emits chunks of bytes over a channel.
|
||||
func (s *Source) Chunks(ctx context.Context, chunksChan chan *sources.Chunk) error {
|
||||
scanErrs := sources.NewScanErrors()
|
||||
|
||||
for _, boardID := range s.boardIDs {
|
||||
board, err := s.getBoard(ctx, boardID)
|
||||
if err != nil {
|
||||
scanErrs.Add(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if err = s.chunkBoard(ctx, board, chunksChan); err != nil {
|
||||
scanErrs.Add(err)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
_ = s.jobPool.Wait()
|
||||
if scanErrs.Count() > 0 {
|
||||
ctx.Logger().V(2).Info("encountered errors while scanning", "count", scanErrs.Count(), "errors", scanErrs)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type board struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type card struct {
|
||||
Name string `json:"name"`
|
||||
Desc string `json:"desc"`
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type action struct {
|
||||
Type string `json:"type"`
|
||||
Data struct {
|
||||
Text string `json:"text"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// Add these two methods to Source
|
||||
func (s *Source) getCards(_ context.Context, boardID string) ([]card, error) {
|
||||
reqURL := fmt.Sprintf("%sboards/%s/cards?key=%s&token=%s", baseURL, boardID, s.apiKey, s.token)
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var cards []card
|
||||
if err := json.NewDecoder(res.Body).Decode(&cards); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return cards, nil
|
||||
}
|
||||
|
||||
func (s *Source) getComments(_ context.Context, cardID string) ([]action, error) {
|
||||
reqURL := fmt.Sprintf("%scards/%s/actions?key=%s&token=%s", baseURL, cardID, s.apiKey, s.token)
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var actions []action
|
||||
if err := json.NewDecoder(res.Body).Decode(&actions); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return actions, nil
|
||||
}
|
||||
|
||||
func (s *Source) getBoard(_ context.Context, boardID string) (*board, error) {
|
||||
reqURL := fmt.Sprintf("%sboards/%s?key=%s&token=%s", baseURL, boardID, s.apiKey, s.token)
|
||||
req, err := http.NewRequest("GET", reqURL, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Accept", "application/json")
|
||||
res, err := s.client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
var b board
|
||||
if err := json.NewDecoder(res.Body).Decode(&b); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func (s *Source) chunkBoard(ctx context.Context, board *board, chunksChan chan *sources.Chunk) error {
|
||||
err := s.chunkItem(ctx, chunksChan, board.ID, board.Desc, board.URL, source_metadatapb.ItemType_ITEM_TYPE_BOARD, board.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cards, err := s.getCards(ctx, board.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, card := range cards {
|
||||
err := s.chunkItem(ctx, chunksChan, card.ID, card.Desc, card.URL, source_metadatapb.ItemType_ITEM_TYPE_CARD, card.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
comments, err := s.getComments(ctx, card.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, comment := range comments {
|
||||
if comment.Type != "commentCard" {
|
||||
continue
|
||||
}
|
||||
|
||||
err := s.chunkItem(ctx, chunksChan, card.ID, comment.Data.Text, card.URL, source_metadatapb.ItemType_ITEM_TYPE_COMMENT, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Source) chunkItem(ctx context.Context, chunksChan chan *sources.Chunk, id, text, url string, itemType source_metadatapb.ItemType, name string) error {
|
||||
data := []byte(text)
|
||||
|
||||
chunk := &sources.Chunk{
|
||||
SourceType: s.Type(),
|
||||
SourceName: s.name,
|
||||
SourceID: s.sourceId,
|
||||
JobID: s.jobId,
|
||||
Data: data,
|
||||
SourceMetadata: &source_metadatapb.MetaData{
|
||||
Data: &source_metadatapb.MetaData_Trello{
|
||||
Trello: &source_metadatapb.Trello{
|
||||
Id: id,
|
||||
Url: url,
|
||||
Type: itemType,
|
||||
Name: name
|
||||
},
|
||||
},
|
||||
},
|
||||
Verify: s.verify,
|
||||
}
|
||||
|
||||
atomic.AddInt64(&s.scannedBytes, int64(len(data)))
|
||||
|
||||
if err := common.CancellableWrite(ctx, chunksChan, chunk); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package trello
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/kylelemons/godebug/pretty"
|
||||
"google.golang.org/protobuf/types/known/anypb"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/context"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/source_metadatapb"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/pb/sourcespb"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/sources"
|
||||
)
|
||||
|
||||
func TestSource_Scan(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*3)
|
||||
defer cancel()
|
||||
|
||||
secret, err := common.GetTestSecret(ctx)
|
||||
if err != nil {
|
||||
t.Fatal(fmt.Errorf("failed to access secret: %v", err))
|
||||
}
|
||||
apiKey := secret.MustGetField("TRELLO_API_KEY")
|
||||
token := secret.MustGetField("TRELLO_TOKEN")
|
||||
|
||||
type init struct {
|
||||
name string
|
||||
verify bool
|
||||
connection *sourcespb.Trello
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
init init
|
||||
wantSourceMetadata *source_metadatapb.MetaData
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "get a chunk",
|
||||
init: init{
|
||||
name: "example board",
|
||||
connection: &sourcespb.Trello{
|
||||
Auth: &sourcespb.Trello_TrelloAuth{
|
||||
ApiKey: apiKey,
|
||||
Token: token,
|
||||
},
|
||||
Boards: []string{"boardID1"},
|
||||
},
|
||||
verify: true,
|
||||
},
|
||||
wantSourceMetadata: &source_metadatapb.MetaData{
|
||||
Data: &source_metadatapb.MetaData_Trello{
|
||||
Trello: &source_metadatapb.Trello{
|
||||
BoardId: "boardID1",
|
||||
BoardName: "Example Board",
|
||||
CardId: "cardID1",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
s := Source{}
|
||||
|
||||
conn, err := anypb.New(tt.init.connection)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if err := s.Init(ctx, tt.init.name, 0, 0, tt.init.verify, conn, 5); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Source.Init() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
|
||||
chunksCh := make(chan *sources.Chunk, 1)
|
||||
go func() {
|
||||
if err := s.Chunks(ctx, chunksCh); (err != nil) != tt.wantErr {
|
||||
t.Errorf("Source.Chunks() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
gotChunk := <-chunksCh
|
||||
if diff := pretty.Compare(gotChunk.SourceMetadata, tt.wantSourceMetadata); diff != "" {
|
||||
t.Errorf("Source.Chunks() %s diff: (-got +want)\n%s", tt.name, diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -68,7 +68,6 @@ func New(c common.Common) *SourceSelect {
|
||||
OssItem("Filesystem", "Scan your filesystem by selecting what directories to scan."),
|
||||
OssItem("AWS S3", "Scan Amazon S3 buckets."),
|
||||
OssItem("CircleCI", "Scan CircleCI, a CI/CD platform."),
|
||||
OssItem("Trello", "Scan Trello, a ticket system."),
|
||||
OssItem("Syslog", "Scan syslog, event data logs."),
|
||||
OssItem("Docker", "Scan a Docker instance, a containerized application."),
|
||||
OssItem("GCS (Google Cloud Storage)", "Scan a Google Cloud Storage instance."),
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/circleci"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/trello"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/docker"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/filesystem"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/sources/gcs"
|
||||
@@ -53,8 +52,6 @@ func GetSourceFields(sourceName string) CmdModel {
|
||||
return syslog.GetFields()
|
||||
case "circleci":
|
||||
return circleci.GetFields()
|
||||
case "trello":
|
||||
return trello.GetFields()
|
||||
case "docker":
|
||||
return docker.GetFields()
|
||||
}
|
||||
|
||||
@@ -1,64 +0,0 @@
|
||||
package trello
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/common"
|
||||
"github.com/trufflesecurity/trufflehog/v3/pkg/tui/components/textinputs"
|
||||
)
|
||||
|
||||
type trelloCmdModel struct {
|
||||
textinputs.Model
|
||||
}
|
||||
|
||||
func GetFields() trelloCmdModel {
|
||||
apiKey := textinputs.InputConfig{
|
||||
Label: "API Key",
|
||||
Key: "api_key",
|
||||
Required: true,
|
||||
Placeholder: "Your Trello API key",
|
||||
}
|
||||
|
||||
token := textinputs.InputConfig{
|
||||
Label: "Token",
|
||||
Key: "token",
|
||||
Required: true,
|
||||
Placeholder: "Your Trello token",
|
||||
}
|
||||
|
||||
boardIds := textinputs.InputConfig{
|
||||
Label: "Board IDs",
|
||||
Key: "board_ids",
|
||||
Required: true,
|
||||
Placeholder: "Comma-separated list of Trello board ID(s)",
|
||||
}
|
||||
|
||||
return trelloCmdModel{textinputs.New([]textinputs.InputConfig{apiKey, token, boardIds})}
|
||||
}
|
||||
|
||||
func (m trelloCmdModel) Cmd() string {
|
||||
var command []string
|
||||
command = append(command, "trufflehog", "trello")
|
||||
|
||||
inputs := m.GetInputs()
|
||||
|
||||
if inputs["api_key"] != "" {
|
||||
command = append(command, "--api-key="+inputs["api_key"])
|
||||
}
|
||||
if inputs["token"] != "" {
|
||||
command = append(command, "--token="+inputs["token"])
|
||||
}
|
||||
if inputs["board_ids"] != "" {
|
||||
command = append(command, "--board-ids="+inputs["board_ids"])
|
||||
}
|
||||
|
||||
return strings.Join(command, " ")
|
||||
}
|
||||
|
||||
func (m trelloCmdModel) Summary() string {
|
||||
inputs := m.GetInputs()
|
||||
labels := m.GetLabels()
|
||||
keys := []string{"api_key", "token", "board_ids"}
|
||||
|
||||
return common.SummarizeSource(keys, inputs, labels)
|
||||
}
|
||||
@@ -186,17 +186,6 @@ message Test {
|
||||
string file = 1;
|
||||
}
|
||||
|
||||
message Trello {
|
||||
string board_id = 1;
|
||||
string card_id = 2;
|
||||
string list_id = 3;
|
||||
string action_id = 4;
|
||||
string user_id = 5;
|
||||
string username = 6;
|
||||
string timestamp = 7;
|
||||
string link = 8;
|
||||
}
|
||||
|
||||
message Jenkins {
|
||||
string project_name = 1;
|
||||
int64 build_number = 2;
|
||||
@@ -305,6 +294,5 @@ message MetaData {
|
||||
SharePoint sharepoint = 25;
|
||||
GoogleDrive googleDrive = 26;
|
||||
AzureRepos azureRepos = 27;
|
||||
Trello trello = 28;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ enum SourceType {
|
||||
SOURCE_TYPE_SHAREPOINT = 29;
|
||||
SOURCE_TYPE_GCS_UNAUTHED = 30;
|
||||
SOURCE_TYPE_AZURE_REPOS = 31;
|
||||
SOURCE_TYPE_TRELLO = 32;
|
||||
}
|
||||
|
||||
message LocalSource {
|
||||
@@ -266,17 +265,6 @@ message Jenkins {
|
||||
}
|
||||
}
|
||||
|
||||
message Trello {
|
||||
message TrelloAuth {
|
||||
string api_key = 1;
|
||||
string token = 2;
|
||||
}
|
||||
|
||||
TrelloAuth auth = 1;
|
||||
repeated string boards = 2;
|
||||
}
|
||||
|
||||
|
||||
message Teams {
|
||||
string endpoint = 1 [(validate.rules).string.uri_ref = true];
|
||||
oneof credential {
|
||||
|
||||
Reference in New Issue
Block a user