diff --git a/pkg/detectors/slack/slack.go b/pkg/detectors/slack/slack.go index eea5571a8c67..2f03cc353ec0 100644 --- a/pkg/detectors/slack/slack.go +++ b/pkg/detectors/slack/slack.go @@ -13,13 +13,16 @@ import ( "github.com/trufflesecurity/trufflehog/v3/pkg/common" ) -type Scanner struct{} +type Scanner struct { + client *http.Client +} // Check that the Slack scanner implements the SecretScanner interface at compile time. var _ detectors.Detector = Scanner{} var ( - tokenPats = map[string]*regexp.Regexp{ + defaultClient = common.SaneHttpClient() + tokenPats = map[string]*regexp.Regexp{ "Slack Bot Token": regexp.MustCompile(`xoxb\-[0-9]{10,13}\-[0-9]{10,13}[a-zA-Z0-9\-]*`), "Slack User Token": regexp.MustCompile(`xoxp\-[0-9]{10,13}\-[0-9]{10,13}[a-zA-Z0-9\-]*`), "Slack Workspace Access Token": regexp.MustCompile(`xoxa\-[0-9]{10,13}\-[0-9]{10,13}[a-zA-Z0-9\-]*`), @@ -53,34 +56,54 @@ func (s Scanner) FromData(ctx context.Context, verify bool, data []byte) (result tokens := tokenPat.FindAllString(dataStr, -1) for _, token := range tokens { - s := detectors.Result{ + s1 := detectors.Result{ DetectorType: detectorspb.DetectorType_Slack, Raw: []byte(token), } if verify { - client := common.SaneHttpClient() + client := s.client + if s.client == nil { + client = defaultClient + } + req, err := http.NewRequestWithContext(ctx, "POST", verifyURL, nil) if err != nil { continue } + req.Header.Add("Content-Type", "application/json; charset=utf-8") req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token)) + res, err := client.Do(req) if err == nil { defer res.Body.Close() var authResponse authRes if err := json.NewDecoder(res.Body).Decode(&authResponse); err != nil { - continue + s1.VerificationError = fmt.Errorf("failed to decode auth response: %w", err) } - s.Verified = authResponse.Ok + + if authResponse.Ok { + s1.Verified = true + // Slack API returns 200 even if the token is invalid. We need to check the error field. + } else if authResponse.Error == "invalid_auth" { + // The secret is determinately not verified (nothing to do) + } else { + s1.VerificationError = fmt.Errorf("unexpected error auth response %+v", authResponse.Error) + } + } else { + s1.VerificationError = err } } - results = append(results, s) + if !s1.Verified && detectors.IsKnownFalsePositive(string(s1.Raw), detectors.DefaultFalsePositives, true) { + continue + } + + results = append(results, s1) } } - return + return results, nil } func (s Scanner) Type() detectorspb.DetectorType { diff --git a/pkg/detectors/slack/slack_test.go b/pkg/detectors/slack/slack_test.go index 63f23f350b97..84c869898302 100644 --- a/pkg/detectors/slack/slack_test.go +++ b/pkg/detectors/slack/slack_test.go @@ -9,13 +9,15 @@ 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" ) -func TestScanner_FromChunk(t *testing.T) { +func TestSlack_FromChunk(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() testSecrets, err := common.GetSecret(ctx, "trufflehog-testing", "detectors2") @@ -24,17 +26,28 @@ func TestScanner_FromChunk(t *testing.T) { } secret := testSecrets.MustGetField("SLACK") secretInactive := testSecrets.MustGetField("SLACK_INACTIVE") + + type args struct { + ctx context.Context + data []byte + verify bool + } + tests := []struct { - name string - data []byte - verify bool - wantResults []detectors.Result - wantErr bool + name string + s Scanner + args args + wantResults []detectors.Result + wantErr bool + wantVerificationErr bool }{ { - name: "found, verified", - data: []byte(fmt.Sprintf("You can find a slack secret %s within", secret)), - verify: true, + name: "found, verified", + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a slack secret %s within", secret)), + verify: true, + }, wantResults: []detectors.Result{ { DetectorType: detectorspb.DetectorType_Slack, @@ -44,9 +57,12 @@ func TestScanner_FromChunk(t *testing.T) { wantErr: false, }, { - name: "found but unverified", - data: []byte(fmt.Sprintf("You can find a slack secret %s within", secretInactive)), - verify: true, + name: "found but unverified", + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a slack secret %s within", secretInactive)), + verify: true, + }, wantResults: []detectors.Result{ { DetectorType: detectorspb.DetectorType_Slack, @@ -55,23 +71,62 @@ func TestScanner_FromChunk(t *testing.T) { }, wantErr: 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 slack secret %s within", secret)), + verify: true, + }, + wantResults: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Slack, + Verified: false, + }, + }, + wantErr: false, + wantVerificationErr: true, + }, + { + name: "unexpected auth response", + s: Scanner{client: common.ConstantResponseHttpClient(200, `{"ok": false, "error": "unexpected_error"}`)}, + args: args{ + ctx: context.Background(), + data: []byte(fmt.Sprintf("You can find a slack secret %s within", secret)), + verify: true, + }, + wantResults: []detectors.Result{ + { + DetectorType: detectorspb.DetectorType_Slack, + Verified: false, + }, + }, + wantErr: false, + wantVerificationErr: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - s := Scanner{} - got, err := s.FromData(context.Background(), tt.verify, tt.data) + got, err := tt.s.FromData(tt.args.ctx, tt.args.verify, tt.args.data) if (err != nil) != tt.wantErr { - t.Errorf("Scanner.FromData) error = %v, wantErr %v", err, tt.wantErr) + t.Errorf("Slack.FromData() error = %v, wantErr %v", err, tt.wantErr) return } + for i := range got { if len(got[i].Raw) == 0 { t.Fatal("no raw secret present") } got[i].Raw = nil + + if (got[i].VerificationError != nil) != tt.wantVerificationErr { + t.Fatalf("wantVerificationError = %v, verification error = %v", tt.wantVerificationErr, got[i].VerificationError) + } } - if diff := pretty.Compare(got, tt.wantResults); diff != "" { - t.Errorf("Scanner.FromData) %s diff: (-got +want)\n%s", tt.name, diff) + ignoreOpts := cmpopts.IgnoreFields(detectors.Result{}, "Raw", "VerificationError") + if diff := cmp.Diff(got, tt.wantResults, ignoreOpts); diff != "" { + t.Errorf("Slack.FromData() %s diff: (-got +want)\n%s", tt.name, diff) } }) }